I have the following type guard:
enum X {
A = "1"
}
const isNullableX = (value: any): value is X | null => false
let foo: string | null = ''
if (isNullableX(foo)) {
console.log(foo)
}
I would expect the type of foo
in the last console.log
to be X | null
but instead, it's only X
. Can someone explain to me how this works? If I change the declaration of foo
to let foo: unknown = ''
then the type in the console.log
is inferred correctly.
Here's an example to play with: Playground
CodePudding user response:
TypeScript uses control flow analysis to narrow the apparent type of an expression to be more specific. For example, when you assign a value to a variable of a union type, it will narrow the type of the variable to just those union members which work for that value, at least until the next assignment. So for this:
let foo: string | null = ''
foo // string
The compiler has already narrowed foo
from string | null
to just string
. The compiler knows that foo
is not null
after that assignment. (If you change this to let foo = Math.random()<0.5 ? '' : null
then the assignment won't narrow and things might behave as you expect later.)
Control flow analysis will either narrow the apparent type of an expression, or reset the type back to the annotated or original type. What you can't realistically do is arbitrarily widen a type, or mutate a type to something unrelated.
When you call a user-defined type guard function like
const isNullableX = (value: any): value is X | null => false
it will, depending on the output, narrow the type of its input. In your call here:
if (isNullableX(foo)) {
foo // X
}
you are narrowing foo
from string
to something assignable to X | null
. The only plausible result here is X
, since null
is not assignable to string
. And so foo
is narrowed to X
. If you were expecting foo
to change from string
to X | null
, that can't happen because it's sort of an arbitrary mutation (I suppose it could be a reset-followed-by-a-narrowing, but there's no resetting because there's no reassignment).
CodePudding user response:
How about this...
enum X {
A = "1"
}
function assertIsTrue(condition: unknown): asserts condition is boolean {
if (!condition) {
throw new Error("Not True!");
}
}
const isNullableX = (value: any): value is X | null => false
let foo = '' as string | null;
assertIsTrue(isNullableX(foo));
console.log(foo);