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
It is possible to violate generic constraints when distributing union typesΒ #63708
typeIssue<A,BextendsA>=Aextendsunknown
? Bextendsunknown
? Show<A,B>
: never
: never;typeShow<A,BextendsA>=[A,B]&{};typeX=Issue<0|1,0|1>;
π Actual behavior
X is evaluated to [0, 0] | [0, 1] | [1, 0] | [1, 1].
No errors are reported despite the [0, 1] | [1, 0] part resulting from the evaluation of Show<0, 1> and Show<1, 0> which should not be allowed because of the B extends A constraint.
π Expected behavior
An error is reported on line 3, saying the constraint might be violated.
Additional information about the issue
This is how the type should actually be implemented:
typeIssue<A,BextendsA>=Aextendsunknown
? BextendsA
? Show<A,B>// only B members that extend the current A member reach this line
: never
: never;
changed the title [-]It is possibleto to violate generic constraints when distributing union types[/-][+]It is possible to violate generic constraints when distributing union types[/+]on Aug 2, 2026
Well, where is the distribution actually occurring? Is Issue distributing to Show<0, 0> | Show<0, 1> | Show<1, 0> | Show<1, 1>? That would violate the constraint, but that's not the only path to the result you're seeing.
If the distribution occurs inside Show, then you're simply constructing Show<0 | 1, 0 | 1>, which satisfies the constraint and produces [0 | 1, 0 | 1], which is equivalent to [0, 0] | [0, 1] | [1, 0] | [1, 1].
In other words, I don't think the constraint has been technically violated. There's a possibility that TS took a wrong turn and happened to end up at the right place anyways. Looking at those extends clauses, I wonder if this is an oversimplification of some genuinely problematic code.
So... is it validating the constraints to Show based on the original types specified for A and B, and then performing the actual substitution for the arguments to Show based on the distributed (narrower) constituents?
In a conditional type A extends B ? XXX : YYY we can (and do) accurately infer the constraint A extends B in type XXX, but we lack the ability to express and infer the opposite constraint in YYY. In theory, negated types would help here as we could infer the constraint A extends not B (which in the case of Show<A, B> would then cause B to fail its B extends A static constraint). However, lacking that ability, we can't (and don't) infer a constraint for A in YYY. That in turn is why the violation described in the issue isn't reported.
But also in Joe Calzaretta (@jcalz)'s example, I think there should be an error. In my opinion, surprising constraint violation like this should never be possible. If TypeScript doesn't have enough information to decide if a constraint is satisfied, an error should be reported so that the developer is forced to check it first like this:
typeIssue<Aextendsnumber,BextendsA>=AextendsB ? never : /* --> */BextendsA ? Show<A,B> : never
The issue is about TypeScript assuming that B still extends A even after A is distributed, and the examples show that this assumption can be wrong. The particular conditional type branch where that leads to problems doesn't really matter.
I agree that assumptions made involving A don't always hold when A is distributed. In the example
typeIssue<Aextendsnumber,BextendsA>=AextendsB ? Show<A,B> : never
it would seem correct to report an error in Show<A, B> that B doesn't extend the distributed A. In a sense, the distributed A should really behave as a new type variable that extends the non-distributed A. Of course we haven't done ourselves any favors by using the same name for the distributed and non-distributed versions of A.
Reacted by Joe Calzaretta, aweebit and Daniel Rosenwasser
Activity
[-]It is possibleto to violate generic constraints when distributing union types[/-][+]It is possible to violate generic constraints when distributing union types[/+]snarbles2 commentedon Aug 3, 2026
Well, where is the distribution actually occurring? Is
Issuedistributing toShow<0, 0> | Show<0, 1> | Show<1, 0> | Show<1, 1>? That would violate the constraint, but that's not the only path to the result you're seeing.If the distribution occurs inside
Show, then you're simply constructingShow<0 | 1, 0 | 1>, which satisfies the constraint and produces[0 | 1, 0 | 1], which is equivalent to[0, 0] | [0, 1] | [1, 0] | [1, 1].In other words, I don't think the constraint has been technically violated. There's a possibility that TS took a wrong turn and happened to end up at the right place anyways. Looking at those
extendsclauses, I wonder if this is an oversimplification of some genuinely problematic code.jcalz commentedon Aug 3, 2026
There's definitely an unsoundness here. The most obvious way I can think of to demonstrate it looks like
Playground link
where TS is apparently happy to evaluate
Show<1, 0>despite the generic constraints being violated.Whether or not this unsoundness amounts to a bug remains to be seen here (kind of guessing it's more trouble than it's worth to deal with it).
snarbles2 commentedon Aug 3, 2026
So... is it validating the constraints to
Showbased on the original types specified forAandB, and then performing the actual substitution for the arguments toShowbased on the distributed (narrower) constituents?ahejlsberg commentedon Aug 27, 2026
In a conditional type
A extends B ? XXX : YYYwe can (and do) accurately infer the constraintA extends Bin typeXXX, but we lack the ability to express and infer the opposite constraint inYYY. In theory, negated types would help here as we could infer the constraintA extends not B(which in the case ofShow<A, B>would then causeBto fail itsB extends Astatic constraint). However, lacking that ability, we can't (and don't) infer a constraint forAinYYY. That in turn is why the violation described in the issue isn't reported.So, as things stand, this is a design limitation.
aweebit commentedon Aug 28, 2026
Anders Hejlsberg (@ahejlsberg) your comment seems to be an answer to Joe Calzaretta (@jcalz)'s comment. In my original example, the
YYYbranches are completely irrelevant.aweebit commentedon Aug 28, 2026
But also in Joe Calzaretta (@jcalz)'s example, I think there should be an error. In my opinion, surprising constraint violation like this should never be possible. If TypeScript doesn't have enough information to decide if a constraint is satisfied, an error should be reported so that the developer is forced to check it first like this:
The issue is about TypeScript assuming that
Bstill extendsAeven afterAis distributed, and the examples show that this assumption can be wrong. The particular conditional type branch where that leads to problems doesn't really matter.ahejlsberg commentedon Aug 28, 2026
I agree that assumptions made involving
Adon't always hold whenAis distributed. In the exampleit would seem correct to report an error in
Show<A, B>thatBdoesn't extend the distributedA. In a sense, the distributedAshould really behave as a new type variable that extends the non-distributedA. Of course we haven't done ourselves any favors by using the same name for the distributed and non-distributed versions ofA.ahejlsberg commentedon Sep 10, 2026
A fix is now available in #64237.