Search code examples
typescriptundefinedstatic-analysistypechecking

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


I am compiling in TypeScript 3.6.2 with strictNullChecks enabled.

Say I declared a variable that may be undefined:

let filename: string|undefined;

Then, a callback may assign a value to it, or leave it undefined:

doIt(() => filename = "assigned");

Now I check whether the callback assigned to filename; otherwise,filenameis undefined and I exit the program (return value ofnever`):

if (filename === undefined) {
  process.exit(0);
}

If this if condition is false, that means that filename must have a valid string value, correct? Finally, I try to use my most-definitely a string:

console.log(filename.toUpperCase());

However, I get an error:

source/repro.ts:6:13 - error TS2532: Object is possibly 'undefined'.

6 console.log(filename.toUpperCase());
              ~~~~~~~~


Found 1 error.

As far as I understand, because of the if statement above having a never return, that means the program terminates before it can reach the following lines that uses filename; therefore, filename must be a string! Am I missing something here? Why does TypeScript still believe it filename could still be undefined after the never return?


For reproduction, here is the full program:

let filename: string|undefined;
doIt(() => filename = "assigned");
if (filename === undefined) {
  process.exit(0);
}
console.log(filename.toUpperCase());
function doIt(fn: () => void) {
  fn();
}

Note: I can solve my issue in my real program since I can initialize filename = "", and check for it in the if statement. However, I'm wondering why this specific approach will not work.


EDIT: here's my tsconfig.json. I tried this example in a brand new folder WITHOUT a tsconfig.json and I could not reproduce this error. Maybe there's something up in my tsconfig, but I haven't pinned it down yet:

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es2017",
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "dist",
    "declaration": true,
    "alwaysStrict": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "noImplicitAny": true,
    "strictBindCallApply": true,
    "strictFunctionTypes": true,
    "strictNullChecks": true,
    "noUnusedLocals": true
  },
  "include": [
    "source/**/*.ts"
  ]
}

Solution

  • 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.