You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Type guards have strange narrowing behavior when other guard branches exhaust union constituents #4029
module A{exportconstenumBrand{}}interfaceA{brand: A.Brand;a: string;}functionisA(value: S): value is A{return'a'in<any>value;}
module B{exportconstenumBrand{}}interfaceB{brand: B.Brand;b: number;}functionisB(value: S): value is B{return'b'in<any>value;}
module C{exportconstenumBrand{}}functionisC(value: S): value is C{returnisA(value)&&isB(value);}interfaceC{brand: C.Brand;a: string;b: number;}typeS=A|B|C;functiontest(state: S){if(isA(state)){// works: state is A}elseif(isB(state)){// works: state is B}elseif(isC(state)){// works: state is C}else{// hey, state is A again! how come?}}
thanks for narrowing down the problem, interestingly if the last possible case isn't explicitly typeguarded it gets deduced correctly
if(typeofx==="string"){// string}elseif(typeofx==="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}elseif(isB(state)){// narrowed to A}elseif(isC(state)){// narrowed to A | B}else{// type begins with to A | B | Cstate// 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}elseif(isB(state)){// narrowed to A}elseif(isC(state)){// narrowed to A | B}else{// type begins with to A | B | Cstate// 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 void. Am I making sense? Would it violate anything in TS architecture?
if(isA(state)){// isA(...) returns A and the narrowing type has the value of A so snap back to A | B | C}elseif(isB(state)){// narrowed to A}elseif(isC(state)){// narrowed to A | B}else{// can we have state of "void" in here?}
Also instanceof doesn't seem to narrow the type correctly :
classA{a: boolean;}classB{b: boolean;}classC{c: boolean;}lets: A|B|C;if(sinstanceofA){s.a;}elseif(sinstanceofB){s.b;}else{s;// type is A | B | C, should be just C}
changed the title [-]union types + type guards look not working in LKG[/-][+]Type guards have strange narrowing behavior when other guard branches exhaust union constituents[/+]on Jul 27, 2015
I'm just wondering how to handle the case when dealing with distinct types(not union types).
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.
Activity
mhegazy commentedon Jul 26, 2015
Tingan Ho (@tinganho) wanna take a look?
tinganho commentedon Jul 27, 2015
Mohamed Hegazy (@mhegazy) I will take a look.
DanielRosenwasser commentedon Jul 27, 2015
This is not a type predicate issue, this applies to all type guards:
zpdDG4gta8XKpMCd commentedon Jul 27, 2015
thanks for narrowing down the problem, interestingly if the last possible case isn't explicitly typeguarded it gets deduced correctly
tinganho commentedon Jul 27, 2015
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.
we could probably just add one check for it:
zpdDG4gta8XKpMCd commentedon Jul 27, 2015
I'd expect from practical standpoint since we exhausted all valid cases the only acceptable type for the final else should be
void. Am I making sense? Would it violate anything in TS architecture?tinganho commentedon Jul 27, 2015
Also
instanceofdoesn't seem to narrow the type correctly :http://www.typescriptlang.org/Playground#src=class%20A%20%7B%0A%09a%3A%20boolean%3B%0A%7D%0Aclass%20B%20%7B%0A%09b%3A%20boolean%3B%0A%7D%0Aclass%20C%20%7B%0A%09c%3A%20boolean%3B%0A%7D%0A%0A%0Alet%20s%3A%20A%20%7C%20B%20%7C%20C%3B%0Aif%20(s%20instanceof%20A)%20%7B%0A%09s.a%3B%0A%7D%0Aelse%20if(s%20instanceof%20B)%20%7B%0A%09s.b%3B%0A%7D%0Aelse%20%7B%0A%09s.b%3B%0A%7D
tinganho commentedon Jul 27, 2015
Aleksey-Bykov it probably makes more sense with
void👍zpdDG4gta8XKpMCd commentedon Jul 27, 2015
hm.. i think there might be something in the specs that clears this out
RyanCavanaugh commentedon Jul 27, 2015
Tingan Ho (@tinganho) See #1719 re:
instanceofnot affectingelseblocks[-]union types + type guards look not working in LKG[/-][+]Type guards have strange narrowing behavior when other guard branches exhaust union constituents[/+]tinganho commentedon Jul 28, 2015
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).
Though, I think it would mean walking up the parent for every variable, if we support distinct types as well.
zpdDG4gta8XKpMCd commentedon Jul 28, 2015
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.
sandersn commentedon Nov 17, 2015
This is fixed in 1.8. When the union is exhausted, the original type is used instead.