Repository navigation
Regression: Generic function returning union of tuples is not assignable to identical type since v4.2 #63019
Description
Activity
RyanCavanaugh commented
on Jan 20, 2026 MemberMore actions🤖 Thank you for your issue! I've done some analysis to help get you started. This response is automatically generated; feel free to 👍 or 👎 this comment according to its usefulness.
Similar Issues
Here are the most similar issues I found
- (71%) microsoft/typescript#43904: [TS 4.2] Narrowing a union type is inconsistent using type guards on different member of union
- (70%) microsoft/typescript#43098: Regression on assignability of generics
- (70%) microsoft/typescript#51180: Type confusion when function returns a union
- (70%) microsoft/typescript#49893: Incorrect type inference of common generic method over union type
- (70%) microsoft/typescript#44093: Regression: Rest parameter typed as union of tuple types behaves differently in 4.3
- (70%) microsoft/typescript#44891: Incorrect widening (constraint fallback?) during inference when union with `undefined` appears in constraint
- (70%) microsoft/typescript#1011: Mixing tuple & generics
- (69%) microsoft/typescript#42935: Regression with unions introduced in typescript 4.2
- (69%) microsoft/typescript#21185: Spurious "A type predicate's type must be assignable to its parameter's type"
- (69%) microsoft/typescript#44315: Generic function does not accept argument of type with union and intersection
- (69%) microsoft/typescript#36053: Returned tuples don't match compatible types when using function union types
- (69%) microsoft/typescript#43185: Type loosening regression with union types in TS 4.2
- (69%) microsoft/typescript#58603: Type union not matched starting from TS 5.1
- (69%) microsoft/typescript#44095: Alias type for indexing a generic object with with transform breaking change in 4.2+ (2nd issue)
- (68%) microsoft/typescript#59108: Regression in types after v5.5
If your issue is a duplicate of one of these, feel free to close this issue. Otherwise, no action is needed.
Possibly Working as Intended
It looks like this behavior might be intentional. Here's what a computer had to say about it.
ReadAis not actually a subtype ofReadBonce you apply the rules for higher-order generic functions.When one generic function type is assigned to another, the target function gets to pick the type argument. Whatever the target picks has to work for every call to the source function. Formally, the checker verifies that for all choices of
Rsupplied by the context, the source’s return type is compatible with the target’s.In
ReadB, the caller is allowed to choose anRthat definitely does not containundefined– for examplestring.const b: ReadB = …; const [done, value] = b<string>(); // value: string (cannot be undefined)So any value that is considered assignable to
ReadBmust guarantee that, whenever the caller picks such anR, the tuple[false, R]never containsundefined.A value of type
ReadAdoes not have to give that guarantee. Because the same type parameterRis also used in the[true, R | undefined]branch, an implementation can satisfyReadAin a way that allowsundefinedto flow into thefalsebranch (for instance by writingreturn [false, undefined as any]and relying onany). That would violate the promiseReadBmakes to its callers.Starting with 4.2 the checker became stricter about this scenario to close that soundness hole, so the earlier “working” behavior was actually unsound. The current error is therefore correct.
Reacted by snarbles2I don't think the AI explanation in the previous comment makes sense (otherwise
ReadAshould also not be a subtype of itself, and the "for instance" example with the type assertion could be used to argue that no type should ever be a subtype of any other type).Anyway, I also noticed that code like this has no type errors:
type Example<R> = [false, R] | [true, R | undefined]; type ReadA = <R>() => Example<R>; type ReadB = <R>() => Example<R>; const test = (_: ReadA): ReadB => _;
That's semantically identical to the example from the issue description, just with the return type factored out into a type alias.
Andarist commented
on Jan 20, 2026 ContributorMore actionsMatt Kantor (@mkantor) that's most likely because the inference can match up the type alias on the source and target sides and just infer between its type arguments (without infering from its structure). So it can easily infer between both Rs directly (when relating generic signatures like this the inference happens between thosse signatures).
Without that, unfortunately inference manages to infer from
[true, R | undefined]source into[false, R]target, resulting with inferringR | undefinedintoR. I suspect that creates the issue later on - given that inference hasReturnTypepriority and that implies combination of gathered candidates (the final inferred candidate becomes the union of all gathered candidates). This part of this theory seems to be confirmed by the fact this works fine:type A = <R>(arg: [false, R] | [true, R | undefined]) => void; type B = <R>(arg: [false, R] | [true, R | undefined]) => void; const test = (_: A): B => _;
Overall, my educated guess is that the problem is that
inferToMultipleTypescan't match up discriminated unions like this to infer between "matching" types.- addedHelp WantedYou can do thisYou can do thisPossible 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 cases
on Feb 3, 2026 - addedDomain: check: Type InferenceRelated to type inference performed during signature resolution or `infer` type resolutionRelated to type inference performed during signature resolution or `infer` type resolution
on Feb 3, 2026 I'd like to investigate whether the inference behavior can be improved
without weakening type safety.The issue seems related to inference across the discriminated union:
the source and target branches with the same discriminant may not be
matched before inferring R, causingR | undefinedto leak into the[false, R]branch.The equivalent version using a type alias works, so I plan to investigate
whether the union members can be matched by their discriminant during
inference. Is this direction reasonable?I also noticed that the tuple version reproduces as far back as TypeScript
3.3, while the object version appears to have changed in 4.2.
🔎 Search Terms
generic function assignability, union return type, tuple union, TS2322, regression v4.2, generic type alias
🕗 Version & Regression Information
⏯ Playground Link
https://www.typescriptlang.org/play/?#code/C4TwDgpgBAShCGATAglAvFAPDAfACgEp0coBtAM3gBsBnCAGlgF0oAfM4AJwFcHY2o3AHaII5AJZCIiJgG4AsAChQkWAkQAhdFlyFiZSrT4wW7Ul16MYA4aIlSZCxUoD0LqAFFOnAPacAXFAAKuDQAORwSMhhUOI0UEI+wFDwNDTiAOZC8ABGVNDAPlAq4ZGaYQB0SgDGPkI0ycAQDdp4APqBZcgEnepaaCRtTq7uyLRFABbwYJD1UADu4sATUD45AFYQ1Y2hNEolakgA8huoGNj4RANQAN5QiHUQgYZ0slAAbtS8nVAAvgJ3B5SQIWCBvT5Ub78di2MSSaR-JwHMondb9HSXfSAx7PaivD5fJ78f7sbHA4o8MEEyFE6wwkRwhyIpQjTzePyBEKqCLqVHRWLxRLJVLpLK5fLFIoHHnHDYaSo1OotJoNVGtDqHRB8nqa1H9QayIA
💻 Code
🙁 Actual behavior
The code fails to compile with error TS2322: Type 'ReadA' is not assignable to type 'ReadB'.
It appears that the compiler incorrectly widens the generic type parameter
Rin the source type.Although
ReadAis defined as[false, R] | ..., the error message reports the source type as having[false, R | undefined].It seems that during the generic signature assignability check, the undefined from the
[true, R | undefined]branch is leaking into the inference ofRfor the entire union, causing thefalsebranch to become[false, R | undefined]. This widened source type is then incompatible with the target's more strict[false, R]branch.🙂 Expected behavior
The code should compile without errors because
ReadAandReadBare structurally identical types.Additional information about the issue
No response