Why can TypeScript not determine that a variable is defined after an if statement that checks for undefined returns never

This is the behaviour in Typescript 3.6.3 and earlier, but it actually works the way you want it to in version 3.7.2; here's a Playground Link to see for yourself. If you switch back and forth between versions using the menu, the error appears and disappears.

If this is necessary for your project then you can upgrade Typescript.


Basically, the problem was that the control-flow graph is determined before type-checking, so at the time the CFG is formed (and reachability is checked), the fact that exit returns never isn't available, and hence the CFG branch where exit is called continues on to the code following the if statement, where the variable is in a possibly-undefined state.

This was raised as an issue on GitHub in December 2016, and according to a response in a different thread,

#12825 Generalize handling of never for returns

  • The control flow graph is formed during binding, but we don't have type data yet
  • We could store all calls at each flow control point and then check them for never returns and check this info for computing types
    • Expensive!
  • Correct analysis would require multiple iterations

So these are some of the reasons it may not have been solved in versions 3.6.3 and earlier.


So the main issue here is that in 3.6 never returning functions did not play into control flow analysis. This feature was implemented in 3.7 by this PR.

I you run your code (with some of the types from the node definitions copied) we can see that it will work in 3.7 but not in 3.6

Also the arrow function that sets the variable really has no bearing on the result. Typescript will not do any control flow analysis on how a callback is called from doIt. This is detailed in thisissue.

Just take care with 3.7 and never retuning functions, the conditions for them to participate in control flow are pretty strict:

A function call is analyzed as an assertion call or never-returning call when

  • the call occurs as a top-level expression statement, and
  • the call specifies a single identifier or a dotted sequence of identifiers for the function name, and
  • each identifier in the function name references an entity with an explicit type, and
  • the function name resolves to a function type with an asserts return type or an explicit never return type annotation.

An entity is considered to have an explicit type when it is declared as a function, method, class or namespace, or as a variable, parameter or property with an explicit type annotation. (This particular rule exists so that control flow analysis of potential assertion calls doesn't circularly trigger further analysis.)

So function expression might not participate in CFA if it does not have an explicit annotation

const exit = () =>  {
  throw new Error()
}

let filename: string | undefined;
if (filename === undefined) {
  exit();
}
console.log(filename.toUpperCase()); // error 

Playground Link With an explict annotation it works:

const exit: () => never = () =>  {
  throw new Error()
}

let filename: string | undefined;
if (filename === undefined) {
  exit();
}
console.log(filename.toUpperCase()); // error 

Playground Link