Repository navigation
It is possible to violate generic constraints when distributing union typesΒ #63708
Description
Activity
- 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
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.There's definitely an unsoundness here. The most obvious way I can think of to demonstrate it looks like
type Issue<A extends number, B extends A> = A extends B ? never : Show<A, B> type Show<A extends number, B extends A> = `${B} extends ${A}` type Hmm = Issue<0 | 1, 0> // ^? type Hmm = "0 extends 1" // <-- amazing!
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).
Reacted by snarbles2 and aweebitSo... 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?Reacted by Joe Calzaretta- addedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on Aug 3, 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.
- addedDesign LimitationConstraints of the existing architecture prevent this from being fixedConstraints of the existing architecture prevent this from being fixedand removedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on Aug 27, 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.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:
type Issue<A extends number, B extends A> = A extends B ? never : /* --> */ B extends A ? Show<A, B> : never
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.I agree that assumptions made involving
Adon't always hold whenAis distributed. In the exampletype Issue<A extends number, B extends A> = A extends B ? Show<A, B> : never
it 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.Reacted by Joe Calzaretta, aweebit and Daniel Rosenwasser- addedPossible ImprovementThe current behavior isn't wrong, but it's possible to see that it might be better in some casesThe current behavior isn't wrong, but it's possible to see that it might be better in some casesand removedDesign LimitationConstraints of the existing architecture prevent this from being fixedConstraints of the existing architecture prevent this from being fixed
on Aug 28, 2026 A fix is now available in #64237.
Reacted by aweebit
π Search Terms
constraint, conditional, distributive, distributed, distribution
π Version & Regression Information
β― Playground Link
https://www.typescriptlang.org/play/?#code/C4TwDgpgBAkgznArhAPAQQDRQEJQgD2AgDsATOKNAPigF5K9CTypFiBrYgewHdiAoKFAD8ORkTIU2nXgKFDRAZQAWvdFmxVB8gFxRiEAG4QATtr0HjJgNz9+oSFBVrMYghJbU6UANqvsALpQAGRQAN4Avrb24NAAGt7wSKgADFAAPlAAjFhpmVlUtkA
π» Code
π Actual behavior
Xis 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 ofShow<0, 1>andShow<1, 0>which should not be allowed because of theB extends Aconstraint.π 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: