Reputation: 2586
I ran into a problem with the TypeScript compiler and would appreciate any help people might be able to offer.
I have reduced the issue original code as much as possible, and I believe the source of the issue is coming from the second if
statement.
// This code generates a compile error.
let val: boolean;
function foo(bar: string | string[] | boolean, qux: string[]): void {
if ('boolean' === typeof bar) {
val = bar;
// At this point TypeScript correctly knows the type of bar is `boolean`
return;
}
// At this point TypeScript correctly knows the type of bar is `string | string[]`
if (!Array.isArray(bar)) {
// Here the compiler understand that `bar` can only be a `string`
bar = [bar];
// This is where things start getting wonky.
//
// Here the compiler thinks `bar` is of type `string | string[] | boolean`
// We have eliminated `boolean` already. We also know that `bar` is not an array because we are
// inside this `if` block.
}
// At this point TypeScript correctly knows the type of bar is `string[]`
val = qux.every(q => bar.includes(q)); // Here the compiler fails.
// semantic error TS2339: Property 'includes' does not exist on type 'false'.
}
Interestingly, by simply changing the structure of the function the compiler becomes happy again. In this function we exit early and return a value instead. The type checks are identical, but because we return the values the type is correctly understood by the compiler.
// This code compiles just fine.
function foo(bar: string | string[] | boolean, qux: string[]): boolean {
if ('boolean' === typeof bar) {
return bar;
}
if (!Array.isArray(bar)) {
return qux.includes(bar);
}
return bar.every(b => qux.includes(b));
}
So after all that, my question is why does both bar = [bar];
and val = qux.every(q => bar.includes(q));
have a different (erroneous) type?
Upvotes: 1
Views: 75
Reputation: 51122
This is somewhat of a puzzle, because on the face of it, Typescript is wrong to give bar
the weaker type in the lambda, and in this specific code there is no unsafe behaviour.
However, in the general case, Typescript is correct to give bar
the weaker type within the lambda even though bar
has the stronger type on the same line as it is used in the lambda. The issue can be demonstrated by the code below:
function demo(x: string | number) {
if(typeof x === 'number') {
x = 'now x is a string';
}
// type is string here, so no error
x.includes('now');
// type is string | number here, so this is an error
let f: Function = () => x.includes('now');
x = 23;
// type is number here
f();
}
This code is not type-safe, because although x
does have type string
where the function is declared, x
actually has the type number
where the function is called; so there is no includes
method. Typescript is therefore right to complain about the code. But this code has the same structure as yours; you used a variable in a lambda, and Typescript does not know the lambda will not be called when that variable later has a different type, so in the lambda's scope the variable has the weaker type.
In theory, Typescript could be designed to know that an array's every
method calls the lambda immediately and then discards it, so that in your specific code the variable is definitely going to be a string each time the lambda is called. However, Typescript's type system is not powerful enough to know what circumstances a lambda will be called in, and it would have to be rather more complicated if it did.
Your second example has no error because your lambda calls qux.includes
inside the lambda, instead of bar.includes
. Since the variable qux
always has type string[]
, and was not narrowed from a weaker type, this cannot cause a problem.
Upvotes: 2