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 guard of union with conditional type not working since 4.3Β #44382
interfaceA<S>{a: S}interfaceB<S>{b: S}interfaceC<S>{c: S}typeU1<S>=A<S>|B<S>|C<S>functionf1<S>(u: U1<S>): u is B<S>|C<S>{returnfalse}functiontest1<S>(x: U1<S>){if(!f1(x)){x.a//OK}}typeCond<S>=Sextendsnumber ? B<S> : C<S>typeU2<S>=A<S>|Cond<S>functionf2<S>(u: U2<S>): u is Cond<S>{returnfalse}functiontest2<S>(x: U2<S>){if(!f2(x)){x.a//ERROR}}
π Actual behavior
The type guard is unable to narrow the type of x to A
π Expected behavior
The type guard should narrow the type of x to A because it is not assignable to Cond<S>
Ryan Cavanaugh (@RyanCavanaugh) Can this bug be prioritize for 4.3.4 please? π
It's a real regression over 4.2. Inferred types are not narrow-able anymore.
This was caused by #43183, because the conditional type Cond<S> gets substituted for its constraint, B<S> | C<S>, in the type of x before it gets narrowed, so we are effectively trying to narrow from A<S> | B<S> | C<S>. Most of the time, thatβs way more useful than trying to narrow with Cond<S>, because it means you can actually discriminate between the two branches of the conditional. Cond<S> just wonβt relate with many useful types during narrowing. However, it will relate to itself, which is what your example is doing, but in 4.3 we can no longer tell thatβs whatβs happening since we substituted the constraint earlier on.
I donβt know if it would make sense for your real code Pedro Pedrosa (@pedro-pedrosa), but a workaround would be to expand the conditional type to a union in your type predicate:
functionf2<S>(u: A<S>|B<S>|C<S>): u is B<S>|C<S>{returnfalse}
The only way I can think to fix this without undoing the very useful improvements of #43183 would be to carry the original, non-substituted type through narrowing (in addition to the usually-more-narrowable constraint-substituted type), trying it if narrowing the constraint-substituted type had no effect. But that sounds like it could be expensive, for what feels like quite an edge case to me.
I donβt know if it would make sense for your real code Pedro Pedrosa (@pedro-pedrosa), but a workaround would be to expand the conditional type to a union in your type predicate:
Fortunately I was able to change it to u is A<S> because in my real example I wasn't exactly trying to figure out if my values where Cond<S>, I was trying to figure out if they were notCond<S>. I didn't think of writing the guards this way because my real example is not just A | Cond, it has a few more types in that union other than A so it was just easier to do u is Cond<S> and negate it.
Coming back to this, I think my earlier analysis was right, and given the existence of a reasonable workaround, I doubt we will want to take on the complexity and performance costs of doing something different here. This is very similar to #47337, which we discussed in a recent design meeting: getNarrowableTypeForReference did exactly what itβs supposed to do, but for reasons that are very hard to predict at the time itβs called, it would have turned out better if it returned the original type. Gabriela Araujo Britto (@gabritto) came up with a workable heuristic for #47337, but for this issue I donβt think there is any such heuristic, as evidenced by the existence of my workaround:
type Cond<S> = S extends number ? B<S> : C<S>
type U2<S> = A<S> | Cond<S>
+ function f2<S>(u: U2<S>): u is B<S> | C<S> {- function f2<S>(u: U2<S>): u is Cond<S> {
return false
}
function test2<S>(x: U2<S>) {
if (!f2(x)) {
x.a //ERROR
}
}
Each of the two signatures highlighted in this diff is reasonable to write, but they work mutually exclusively based on whether getNarrowableTypeForReference returns A<S> | B<S> | C<S> or A<S> | Cond<S> for U2<S>. To βfixβ the narrowing ability of one of these type guards by tweaking getNarrowableTypeForReference would be to break the other.
When we discussed #47337, Anders Hejlsberg (@ahejlsberg) noted that weβre doing with getNarrowableTypeForReference is only an approximation of what we should be doing in theory: rather than narrowing the type parameter itself, we ought to be narrowing its constraint. I think that theoretical solution would fix this issue too, but it was decided that was too complex. So, I think this can safely be called a design limitation. Anders Hejlsberg (@ahejlsberg) does that sound right to you, or did I miss something?
Activity
bbenezech commentedon Jun 17, 2021
Ryan Cavanaugh (@RyanCavanaugh) Can this bug be prioritize for
4.3.4please? πIt's a real regression over
4.2. Inferred types are not narrow-able anymore.RyanCavanaugh commentedon Jun 17, 2021
This doesn't meet our bar for a patch release, but I'll move it to 4.4
andrewbranch commentedon Jul 23, 2021
This was caused by #43183, because the conditional type
Cond<S>gets substituted for its constraint,B<S> | C<S>, in the type ofxbefore it gets narrowed, so we are effectively trying to narrow fromA<S> | B<S> | C<S>. Most of the time, thatβs way more useful than trying to narrow withCond<S>, because it means you can actually discriminate between the two branches of the conditional.Cond<S>just wonβt relate with many useful types during narrowing. However, it will relate to itself, which is what your example is doing, but in 4.3 we can no longer tell thatβs whatβs happening since we substituted the constraint earlier on.I donβt know if it would make sense for your real code Pedro Pedrosa (@pedro-pedrosa), but a workaround would be to expand the conditional type to a union in your type predicate:
The only way I can think to fix this without undoing the very useful improvements of #43183 would be to carry the original, non-substituted type through narrowing (in addition to the usually-more-narrowable constraint-substituted type), trying it if narrowing the constraint-substituted type had no effect. But that sounds like it could be expensive, for what feels like quite an edge case to me.
pedro-pedrosa commentedon Jul 26, 2021
Fortunately I was able to change it to
u is A<S>because in my real example I wasn't exactly trying to figure out if my values whereCond<S>, I was trying to figure out if they were notCond<S>. I didn't think of writing the guards this way because my real example is not justA | Cond, it has a few more types in that union other thanAso it was just easier to dou is Cond<S>and negate it.Thanks for the suggestion.
andrewbranch commentedon Feb 3, 2022
Coming back to this, I think my earlier analysis was right, and given the existence of a reasonable workaround, I doubt we will want to take on the complexity and performance costs of doing something different here. This is very similar to #47337, which we discussed in a recent design meeting:
getNarrowableTypeForReferencedid exactly what itβs supposed to do, but for reasons that are very hard to predict at the time itβs called, it would have turned out better if it returned the original type. Gabriela Araujo Britto (@gabritto) came up with a workable heuristic for #47337, but for this issue I donβt think there is any such heuristic, as evidenced by the existence of my workaround:Each of the two signatures highlighted in this diff is reasonable to write, but they work mutually exclusively based on whether
getNarrowableTypeForReferencereturnsA<S> | B<S> | C<S>orA<S> | Cond<S>forU2<S>. To βfixβ the narrowing ability of one of these type guards by tweakinggetNarrowableTypeForReferencewould be to break the other.When we discussed #47337, Anders Hejlsberg (@ahejlsberg) noted that weβre doing with
getNarrowableTypeForReferenceis only an approximation of what we should be doing in theory: rather than narrowing the type parameter itself, we ought to be narrowing its constraint. I think that theoretical solution would fix this issue too, but it was decided that was too complex. So, I think this can safely be called a design limitation. Anders Hejlsberg (@ahejlsberg) does that sound right to you, or did I miss something?