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 of
never`):
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"
]
}
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.