-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Type guard failures with expressions that can use for type guards #9862
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Looks like same thing as #9861. It's due to the class A<T> {
private _: T;
}
class B<T> {
private _: T;
}
function f() {
var o: A<void> | B<void> = new A<void>();
o; // A<void>
o instanceof A || o instanceof B;
o; // A<any>, should be A<void> | B<void>
} I'm not sure why it changes from |
you are right. it should be |
Another repro: function f() {
var s: Set<string> | Set<number>;
s = new Set<number>();
s; // Set<number>
s instanceof Set;
s; // Set<number>
s = new Set<number>();
s; // Set<number>
s instanceof Set || s instanceof Set;
s; // Set<any> <===== inferred type changed
s = new Set<number>();
s; // Set<number>
s instanceof Set && s instanceof Set;
s; // Set<any> <===== inferred type changed
s = new Set<number>();
s; // Set<number>
s instanceof Set, s instanceof Set;
s; // Set<number>
s = new Set<number>();
s; // Set<number>
typeof s === 'object' || typeof s === 'object';
s; // Set<number>
} |
In master, the inferred type is now |
Surprisingly, it is supposed to work this way! And after a lot of discussion with @ahejlsberg I think I can explain why. You need to understand three facts:
Example: If control flowfunction f(x: string | number) {
if (typeof x === 'string') {
throw new Error("I don't handle strings after all!");
}
else {
console.log('numbers are ok');
}
return x; // x: number
} The then-branch of the Complete exampleNow let's look at a miniature version of the examples above: function f(s: Set<string> | Set<number>) {
s = new Set<number>();
s instanceof Set || s instanceof Set;
s; // Set<number> | Set<any>
} The type of the lone |
I understood, thanks. |
Thanks @sandersn for the explanation, which makes it much clearer what's going on. But isn't the compiler incorrectly performing narrowing in a clearly side-effect-free context? All of the following statements, on their own, cause
You've pointed out the reason for this in the current implementation, but perhaps there's scope for future improvement here? The core issue is the compiler's arguably strange 'narrowing' inference below: let u = new Set<number>();
if (u instanceof Set) {
u // u is Set<any> !?
// but we already knew u was a Set<number>,
// so this type guard has widened it, not narrowed it!
} That leads to the following version that I find hard to describe as 'working as intended': function f(s: Set<string> | Set<number>) {
// (1)
s = new Set<number>(); // s is Set<number> after this assignment
if (s instanceof Set) { } // Clearly no side-effects here. No possible changes to s.
s.add(42); // ERROR: Cannot invoke an expression whose type lacks a call signature.
// inferred type of x here is Set<number> | Set<any>
// (2)
s = new Set<number>(); // s is Set<number> after this assignment
if (s instanceof Promise) { } // Clearly no side-effects here. No possible changes to s.
s.add(42); // Works! No error this time!?
// inferred type of x here is Set<number> | (Set<number> & Promise<any>)
} |
Another example of // This part looks good:
let s: Set<string> | Set<number>;
if (s instanceof Set) {
s // s is Set<string> | Set<number>, as expected
}
else {
s.add(42); // ERROR 'add' does not exist on never, as expected
}
// This is not so good:
s = new Set<number>();
if (s instanceof Set) {
s // s is Set<any>
}
else {
s.add(42); // s is Set<number>
// ^^^ no error this time, even though we couldn't possibly have a Set instance here
} |
In your (2), @ahejlsberg has a PR (merged, I think?) that removes the weird behaviour when narrowing would produce never but instead unions on the guarded type plus the original type. It now produces never, which means that (2) would produce the correct type. I think it would fix the else-branch of your example (4) as well. Maybe @ahejlsberg can comment on the efficiency (and design) concerns of analyzing blocks so that empty blocks would not have the bad behaviour in (1) and the others. I don't understand the overall design well enough to say. |
I also also don't get why an un-initialized variable in (3) doesn't widen to Set but the assignment does in (4). I think it might be due to quirks in the handling of unions versus single (normal) types, but I'd have to read through the code to make sure. |
TypeScript Version: 2.0.0
Code
Expected behavior:
A type of
o
after expr isA<void> | B<void>
.Actual behavior:
A type of
o
after expr isA<any>
.The text was updated successfully, but these errors were encountered: