Skip to content

Design Meeting Notes for 3/4/2016 #7395

Closed
@DanielRosenwasser

Description

@DanielRosenwasser

Non-nullable types (#7140)

  • One of the big things we've wanted to do with the type system.
    • A little bit of work left on the PR itself.
  • New compiler switch.
    • --strictNullChecks.
  • Introduces two new (accessible) types called null and undefined.
    • Writing T? is the same as writing T | null | undefined.

Non-immediate assignments

  • JS problem: all variables, by default, have the value undefined:

    var x: number;
    // ...
    x = 1;
    • That's still valid!
    • For that reason, we do some basic assignment-before-use checking.
      • "Rudimentary" at this point.

      • "Perfect" support would require full data-flow analysis.

        • Function hoisting makes this hard.
      • Even Flow doesn't do full data-flow analysis.

      • Though they do control-flow and "type"-flow analysis.

        function (x: string | number) {
            if (typeof x === "string") {
                x = +x;
            }
            // Type of 'x' here is 'number'.
        }
      • Hasn't been a problem so far, but it might start to be a problem for us?

      • Type-flow analysis is in some sense a very general form of data-flow analysis, so...

      • We're going to keep it in mind, but it's not clear what kind of performance implications it would have.

Type guards

  • Standard checks become type guards.

    // Compiled with --strictNullChecks
    declare function f(x: number): string;
    let x: number?;
    if (x) {
        f(x);  // Ok, type of x is number here
    }
    else {
        f(x);  // Error, type of x is number? here
    }
    let a = x != null ? f(x) : "";  // Type of a is string
    let b = x && f(x);  // Type of b is string?
    • x, x == null, x == undefined all have the same behavior.
    • x === undefined and x === null have differing respective behavior.
    • What happens for teams that strictly use null or undefined and use ===/!==?
      • We actually wouldn't be using optionals within the compiler because we only use undefined!
      • We would make a type Maybe<T> = T | undefined.
      • Maybe we should add that and type Nullable<T> = T | null to lib.d.ts.
  • Dotted names in type guards

    interface Options {
        location?: {
            x?: number;
            y?: number;
        };
    
    function foo(options?: Options) {
        if (options && options.location && options.location.x) {
            const x = options.location.x;  // Type of x is number
        }
    }
    • Works on optionals.

Arrays

  • How do we deal with arrays and out-of-bounds?
    • These are simply places we cannot solve the problem.
    • The solution makes things insane to deal with.
    • The cure is worse than the disease.

Behavior of widening and contextual typing

  • Widening is no longer performed when using this new flag.
  • Users now just get the undefined or null types when using these types.
    • So they won't be able to read from them.
    • So they'll have to explicitly opt out.
  • Does that mean we don't need --noImplicitAny?
    • Almost!
    • Empty array literals are still an issue.
      • So --noImplicitAny forces you to decide what to do with empty arrays.
    • Why would they not just become undefined[]?
      • var a: number[] = [];
  • How does this interact with the plan to "contextually type" undefined/null?
    • We were saying this was necessary for contextually implementing class properties.
    • Probably shouldn't be impacted much by this.

Non-null assertion operator

  • A new ! postfix expression-level operator.

  • Produces no runtime-dependent code.

    // Compiled with --strictNullChecks
    function validateEntity(e: Entity?) {
        // Throw exception if e is null or invalid entity
    }
    
    function processEntity(e: Entity?) {
        validateEntity(e);
        let s = e!.name;  // Assert that e is non-null and access name
    }
    • Why can't a type predicate take care of that?
    • It could but it's just cumbersome for some scenarios.

Optionals

  • Optional properties and parameters have fairly intuitive behavior.

    function foo(x?: number) {
    
    }
    • That implicitly gives x the type number | undefined.
  • What about default parameters?

    function foo(x = 15) {
    
    }
    • Externally, x is number | undefined.
      • Declaration files will reflect this.
    • Internally, x has type number.
      • It's as if you had implicitly done the checking you needed (e.g. if (x === undefined) { x = 15; }).
      • Same work has been done for destructuring defaults as well.

Best common type issues

function f(x: boolean) {
    if (x) {
        return 100;
    }
    return undefined;
}
  • There's no best common type between undefined and number, so this caused an error.
  • This is a special case we need to start considering.
    • Enum member types may need this if we choose to adopt them.

Adoption issues

  • Non-null checks are an all-or-nothing sort of feature.
  • Tons of declaration files have been written without this feature in mind (because, well, they couldn't).
  • Becomes a game of wack-a-mole for consumers in patching up declaration files.
  • We could consider a file-specific understanding, or use a tri-state of nullability, but this actually becomes a mental burden for both users and ourselves.
  • Why does this get a switch but not readonly?
    • Meaningful work we can do with readonly that's backwards compatible.
      • Old code doesn't change semantics.
    • Nullability doesn't really have a good story for backwards compatibility - you're now changing the semantics of the old syntax.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design NotesNotes from our design meetings

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions