TypeScript: Uninitialized variables work around strictNullChecks

TypeScript Version: 2.2.0

--strictNullChecks must be used.

Code

interface Foo {
    name: () => string;
}

let foo: Foo;
setTimeout((() => foo.name()), 0);

Expected behavior:

Error at let foo: Foo line since the compiler is implicitly doing let foo: Foo = undefined;.

If I were to write the = undefined initializer myself, the compiler properly errors with:

Type 'undefined' is not assignable to type 'Foo'.

Actual behavior:

The code compiles and errors at runtime:

TypeError: foo is undefined

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 4
  • Comments: 17 (6 by maintainers)

Most upvoted comments

@RyanCavanaugh Why was this closed? Those fixes don’t cover this particular issue.

I think it’s especially important to revisit this issue now that --strictPropertyInitialization is available. With --strictPropertyInitialization, this code correctly emits a compile error:

Link

class Foo {
  private x: number; // Compile-time error!
  foo() {
    // `x` isn't initialized!
    // But it's OK, compile-time error above.
    return this.x.toString();
  }
}

But this code compiles successfully:

Link

function fooFactory() {
  let x: number;
  return {
    foo() {
      // `x` isn't initialized!
      // Run-time error!
      return x.toString();
    }
  };
}

Shouldn’t there be a --strictLocalInitialization mode to catch errors like this? I imagine it would work similarly to --strictPropertyInitialization: if a local variable is not synchronously initialized before it’s used, a compile error would be emitted. To resolve it, one would need to:

A) Initialize the variable in a way that the TypeScript compiler understands (e.g. immediately, or before referencing it in a function somewhere).

-OR-

B) Use a definite assignment assertion. (It seems that this issue would come up naturally when discussing definition assignment assertions for local variables.)

-OR-

C) Explicitly mark the variable as potentially undefined.


Expanding upon option (B), this code was given as an example in the linked-to blog:

let x!: number[];
initialize();
x.push(4);

function initialize() {
    x = [0, 1, 2, 3];
}

I think that this is a misleading example. That code - in an ideal world where the compiler performs flow analysis - should not need the definite assignment assertion.

However, this code should (and currently does not):

let x: number[]; // Doesn't require definite assignment assertion.
function foo() {
    x.toString();
}
foo(); // No compile-time error, but throws an exception.

Thoughts?

Sure, but I’m not enabling --strictNullChecks and using TypeScript for the practicality and free-wheeling spirit of initializing variables however I please 😉 . When I’ve declared a variable to be of a particular type, I’ve explicitly expressed the desire for undefined to not be considered a member of that type, and I assign undefined to that variable somewhere in my program (regardless of if I potentially re-assign to a valid type elsewhere, such as in your if/else example), I expect a reasonable compiler to provide me with an error.

I’m sure there are cases where this necessitates additional ! operators throughout the code, but that’s to be expected if indeed a particular Object “is possibly ‘undefined’”.

let num_or_undefined:number
function runtimeerror() { num_or_undefined.toString() }
runtimeerror()

How is this not a compile error in strict mode? How can i make it error?

let num_or_undefined:number|undefined works as intended, but i have to remember to put |undefined everywhere.

I feel like maybe line 1 should be an error? Something like Type 'undefined' is not assignable to type 'number'. (it would be better if it could just be automatically considered ‘possibly undefined’ though)

(typescript’s not making me feel very type safe)

@gcnew I’m not sure I follow. The following code seems perfectly reasonable to me:

let self = {
    x: 4,
    doSomething: () => self.x + self.x
};

self would have type {x: Number, doSomething: () => Number}, which the () => self.x + self.x expression captures and everything should typecheck cleanly.

Here’s a thornier example that currently breaks even with const:

function indirection(func: () => Number): Number {
    return func();
}

const foo: {a: Number, b: Number} = {
    a: 1,
    b: indirection(() => foo.a),
};

This results in a runtime TypeError because foo is undefined when the callback for b is invoked.

Here’s how I would expect things to work. I might’ve gotten things horribly wrong so please correct me if so:

  1. In strictNullChecks, given a declaration let x: T or const x: T where undefined ∉ T, x must be immediately initialized.
  2. In the initializer for x, function definitions that capture x are split into 2 types:
    1. Simple definitions where the function cannot be passed to anything else will have x: T in their body. This means that the doSomething: () => self.x + self.x from your example has self: {x: Number, doSomething: () => Number}.
    2. If the function //can// be passed to something else, the compiler can no longer reason about when the function gets called. Thus, for the body of the function, x: T | undefined. For my example, b: indirection(() => foo.a), inside the function foo: {a: Number, b: Number} | undefined, at which point the compiler can complain about foo.a because it no longer typechecks.

Point 1 can be made palatable with sugar for IIFE’s for complicated initialization.

Point 2.1 should capture the bulk of self referencing function definitions and things just work. This is because the compiler knows that those functions cannot be invoked until after the variable has been initialized.

Point 2.2 is necessary to ensure correctness. The current version of the compiler breaks with a runtime TypeError, which seems to fly in the face of TypeScript’s primary goal.