-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Type guards have strange narrowing behavior when other guard branches exhaust union constituents #4029
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
@tinganho wanna take a look? |
@mhegazy I will take a look. |
This is not a type predicate issue, this applies to all type guards: var x: boolean | string | number;
if (typeof x === "string") {
// string
}
else if (typeof x === "number") {
// number
}
else if (typeof x === "boolean") {
// boolean
}
else if (typeof x === "string") {
// string
}
else {
// ...number?
} |
thanks for narrowing down the problem, interestingly if the last possible case isn't explicitly typeguarded it gets deduced correctly if (typeof x === "string") {
// string
}
else if (typeof x === "number") {
// number
}
//else if (typeof x === "boolean") {
// // boolean
//}
//else if (typeof x === "string") {
// // string
//}
else {
// boolean as aexpected
} |
The root cause is that it narrows down to 1 type. There is nothing that handles if the narrowing type is the same as narrowed type by an if clause. if (isA(state)) { // narrowed to A again
}
else if (isB(state)) { // narrowed to A
}
else if (isC(state)) { // narrowed to A | B
}
else { // type begins with to A | B | C
state // is A
} we could probably just add one check for it: if (isA(state)) { // isA(...) returns A and the narrowing type has the value of A so snap back to A | B | C
}
else if (isB(state)) { // narrowed to A
}
else if (isC(state)) { // narrowed to A | B
}
else { // type begins with to A | B | C
state // is A | B | C
} |
I'd expect from practical standpoint since we exhausted all valid cases the only acceptable type for the final else should be if (isA(state)) { // isA(...) returns A and the narrowing type has the value of A so snap back to A | B | C
}
else if (isB(state)) { // narrowed to A
}
else if (isC(state)) { // narrowed to A | B
}
else {
// can we have state of "void" in here?
} |
Also class A {
a: boolean;
}
class B {
b: boolean;
}
class C {
c: boolean;
}
let s: A | B | C;
if (s instanceof A) {
s.a;
}
else if(s instanceof B) {
s.b;
}
else {
s;// type is A | B | C, should be just C
} |
@Aleksey-Bykov it probably makes more sense with |
hm.. i think there might be something in the specs that clears this out |
I just submitted a PR to handle void narrowing #4051. I'm just wondering how to handle the case when dealing with distinct types(not union types). let x: number;
if (typeof x === "number") {
}
else {
x// is what? void?
} Though, I think it would mean walking up the parent for every variable, if we support distinct types as well. |
TLDR: should be void Isn't a distinct case just a special case of a union of one? If so, then void in the else block is quite expected. More to that, if you think about it, what else can it be? It's declared to be of a certain type and this is what we just asserted. According to its declaration it cannot be anything else. So the else block is technically unreachable and doesn't make sense, because the clause always holds true as long as the type system is sound. I'd suggested to disallow such if statements altogether due to lack of sense they make. |
This is fixed in 1.8. When the union is exhausted, the original type is used instead. |
The text was updated successfully, but these errors were encountered: