Repository navigation
Type guard failures that become an intersection type #9859
Description
Activity
I'd say this follows from #8911/#9016.
ois narrowed toB<void>when you assignnew B<void>too. Then the compiler sees theo instanceof Aas further narrowing and comes up with the intersection type.I suspect the
A & Btype, which seems impossible in terms runtime behaviour, seems fine to the compiler which is just concerned about the structural intersection of theAandBtypes.Note this comment in particular: #8911 (comment)
then options here are either
neverorA & B.neversince this code can not execute ifAandBare indeed disjoint, orA & BifBextendsAat runtime. The first is too restrictive.- addedWorking as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bug
on Jul 21, 2016 falsandtru commented
on Jul 21, 2016 ContributorAuthorMore actionsThis problem seems to caused by declared type internal overwriting. This behavior should be a bug because following code works correctly.
class A<T> { private _: T; } class B<T> { private _: T; } function f() { var o: A<void> | B<void>; if (o instanceof A) return o; // A<void> }
I am not sure i understand what you meant there. but the important difference between the two samples is the assignment...
var o: A<void> | B<void> = new B<void>(); // here o can only be `B<void>` and nothing else. no assignments has happened to o since it got `new B<void>()` if (o instanceof A) return o; /// now how can we narrow B into A?
falsandtru commented
on Jul 21, 2016 ContributorAuthorMore actionsMohamed Hegazy (@mhegazy) why do you think "working as intended" about that difference? The two samples should be a same narrowed type by type guard.
falsandtru (@falsandtru) assignments cause narrowing as per #8010. Then subsequent narrowing to an unrelated type narrows to the intersection of the two narrowed types as per #9031. According to those two rules, your example is indeed working as intended.
Your sample is a bit like the following, which produces the same intersection narrowed type:
class A<T> { private _: T; } class B<T> { private _: T; } function f() { var o: A<void> | B<void>; if (o instanceof B) { if (o instanceof A) return o; // B<void> & A<any>, should be A<void> } }
falsandtru commented
on Jul 22, 2016 ContributorAuthorMore actionsI understood that behavior, but I'm not sure that behavior is a spec, or a bug.
Why would it be a bug? Control flow analysis can infer:
- That
ois aBthroughout the body off, since aBis assigned tooand there are no subsequent assignments that could change it to something else. - That the only way we could reach the guarded
return ostatement is ifois anA. And sinceois already known to be aB, thenomust be anA & Bin that guarded clause.
- That
falsandtru commented
on Jul 22, 2016 ContributorAuthorMore actionsMerging #9861.
class A<T> { private _: T; } class B<T> { private _: T; } function f() { var o: A<void> | B<void> = new B<void>(); if (!(o instanceof B)) return o; // B<void>, should be A<void> o; // B<any>, should be B<boid> }
Merging #9862.
class A<T> { private _: T; } class B<T> { private _: T; } function f() { var o: A<void> | B<void> = new A<void>(); o instanceof A || o instanceof B; o; // A<any>, should be A<void> | B<void> }
the first case is again because of the initialization. there is no way for
oto be any thing butB. so checking!(o instanceof B)is really not meaningful here. so you haveB, you take outB, what are you left with..The same of the second case, it is A, it can not be anything else.
falsandtru commented
on Jul 22, 2016 ContributorAuthorMore actionsI see, I think we can close this issue. However, a compiler option for detection of this assignment (as initialization) type mismatch is helpful to avoid these confusing. A declared type shouldn't be wider than an assigned type with initialization in these CFA behaviors.
A declared type shouldn't be wider than an assigned type
A wider declared type allows for re-assignment later on:
let a: string | number = 'foo'; a; // a is narrowed to string here a = 42; a; // a is narrowed to number here
falsandtru commented
on Jul 22, 2016 ContributorAuthorMore actionsIt case seems an anti-pattern.
falsandtru (@falsandtru) why an anti-pattern?
falsandtru commented
on Jul 22, 2016 ContributorAuthorMore actionsThere's plenty of discussion about this in #8513. Note the realistic motivating example involves modelling an
Optional<T>type as a union ofSome<T>andNone, and some control flow that initialises anOptionaltoNoneand then possibly reassigns it with aSomevalue.I don't think you can simply say
It should be
const a: string = 'foo'; const b: number = 42;
in light of legitimate examples where people are trying to model runtime-valid code using TypeScript, where union variables with initializers occur naturally as a result.
falsandtru commented
on Jul 22, 2016 ContributorAuthorMore actionsMy example is based on immutable programing. So I suggested as optional in #9886.
OK, then if you make the variable in your first example immutable:
function f() { const o: A<void> | B<void> = new B<void>(); if (o instanceof A) return o; // B<void> & A<any>, should be A<void> }
...then it seems an even stronger case for the current compiler behaviour being correct, doesn't it? Or have I misunderstood what you mean by immutable?
falsandtru commented
on Jul 22, 2016 ContributorAuthorMore actionsIt example is a bad case. So I said "But this operation is a mistake, should be removed." in #9886. My suggestion is useful for finding mistakes like this.
- locked and limited conversation to collaborators
on Jun 19, 2018
TypeScript Version: 2.0.0
Code
Expected behavior:
A type of
onarrowed by type guard isA<void>.Actual behavior:
A type of
onarrowed by type guard isB<void> & A<any>.