microsoft/TypeScript

Always assigning to an un-initialized variable in an exhaustive switch

apexskier opened this issue ยท 3 comments

TypeScript Version: 3.4.0-dev.20190220

Search Terms:
switch case exhaustive assignment assign undefined TS2322

Code
Compile with the flags --noImplicitReturns --noImplicitAny --strictNullChecks

enum Test {
    A,
    B,
    C
}

// this example correctly understands that all possible values of t are handled
function foo(t: Test): number {
    switch (t) {
        case Test.A:
            return 1;
        case Test.B:
            return 2;
        case Test.C:
            return 3;
    }
}

// this should be identical to foo, since a is always assigned as the switch is exhaustive
function bar(t: Test): number {
    let a;
    switch (t) {
        case Test.A:
            a = 1;
        case Test.B:
            a = 2;
        case Test.C:
            a = 3;
    }
    return a; // I don't expect an error here
}

Expected behavior:
Code compiles with no errors. When a is returned in bar, its type is number. Typescript understands that an exhaustive switch with each case assigning to a type-inferred variable means that the variable's type is the union of each assigned value's type. I'd expect this to work because returns within an exhaustive switch are understood in a similar manner.

Actual behavior:
When a is returned in bar, its type is number | undefined

TS2322: Type 'number | undefined' is not assignable to type 'number'.
  Type 'undefined' is not assignable to type 'number'.

Playground Link:
link here
Turn on noImplicitReturns, noImplicitAny, and strictNullChecks.

Related Issues:

  • #22470 (different, since the default doesn't assign)
  • #18362 (this is pretty old, from TS 2.4.2. Since foo in my example works I think the closure reason might be invalid)

Comments in #18362 still apply. We have some special-casing we're capable of doing for return, but it doesn't generalize to assignments.

how is it not covered by officially recommended:

function noMore(_: never): never { throw new Error('Boom!'); }

// this should be identical to foo, since a is always assigned as the switch is exhaustive
function bar(t: Test): number {
  let a;
  switch (t) {
    case Test.A:
      a = 1;
      break;
    case Test.B:
      a = 2;
      break;
    case Test.C:
      a = 3;
      break;
    default: return noMore(t);
  }
  return a; // <-- works
}

?