Skip to content

Regression: Generic function returning union of tuples is not assignable to identical type since v4.2 #63019

Description

@ikeyan

🔎 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

type ReadA = <R>() => [false, R] | [true, R | undefined];
type ReadB = <R>() => [false, R] | [true, R | undefined];

// Error: Type 'ReadA' is not assignable to type 'ReadB'.
const test = (_: ReadA): ReadB => _;

// Also happens with object types
type ReadObjA = <R>() => { done: false; value: R } | { done: true; value: R | undefined };
type ReadObjB = <R>() => { done: false; value: R } | { done: true; value: R | undefined };

// Error: Type 'ReadObjA' is not assignable to type 'ReadObjB'.
const testObj = (_: ReadObjA): ReadObjB => _;

🙁 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 R in the source type.
Although ReadA is 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 of R for the entire union, causing the false branch to become [false, R | undefined]. This widened source type is then incompatible with the target's more strict [false, R] branch.

Type 'ReadA' is not assignable to type 'ReadB'.
  Type '[true, R | undefined] | [false, R | undefined]' is not assignable to type '[false, R] | [true, R | undefined]'.
    Type '[false, R | undefined]' is not assignable to type '[false, R] | [true, R | undefined]'.
      Type '[false, R | undefined]' is not assignable to type '[false, R]'.
        Type at position 1 in source is not compatible with type at position 1 in target.
          Type 'R | undefined' is not assignable to type 'R'.
            'R' could be instantiated with an arbitrary type which could be unrelated to 'R | undefined'.(2322)
Type 'ReadObjA' is not assignable to type 'ReadObjB'.
  Type '{ done: false; value: R | undefined; } | { done: true; value: R | undefined; }' is not assignable to type '{ done: false; value: R; } | { done: true; value: R | undefined; }'.
    Type '{ done: false; value: R | undefined; }' is not assignable to type '{ done: false; value: R; } | { done: true; value: R | undefined; }'.
      Type '{ done: false; value: R | undefined; }' is not assignable to type '{ done: false; value: R; }'.
        Types of property 'value' are incompatible.
          Type 'R | undefined' is not assignable to type 'R'.
            'R' could be instantiated with an arbitrary type which could be unrelated to 'R | undefined'.(2322)

🙂 Expected behavior

The code should compile without errors because ReadA and ReadB are structurally identical types.

Additional information about the issue

No response

Activity

  1. mkantor commented on Jan 20, 2026

    @mkantor
    Contributor

    Just a note: you mentioned that the behavior changed in v4.2, but it seems only the object-type-returning version changed. The tuple version has an error as far back as v3.3.

    Some playgrounds to demonstrate:

  2. RyanCavanaugh commented on Jan 20, 2026

    @RyanCavanaugh
    Member

    🤖 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

    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.

    ReadA is not actually a subtype of ReadB once 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 R supplied by the context, the source’s return type is compatible with the target’s.

    In ReadB, the caller is allowed to choose an R that definitely does not contain undefined – for example string.

    const b: ReadB = …;
    const [done, value] = b<string>();    // value: string (cannot be undefined)
    

    So any value that is considered assignable to ReadB must guarantee that, whenever the caller picks such an R, the tuple [false, R] never contains undefined.

    A value of type ReadA does not have to give that guarantee. Because the same type parameter R is also used in the [true, R | undefined] branch, an implementation can satisfy ReadA in a way that allows undefined to flow into the false branch (for instance by writing return [false, undefined as any] and relying on any). That would violate the promise ReadB makes 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.

  3. mkantor commented on Jan 20, 2026

    @mkantor
    Contributor

    I don't think the AI explanation in the previous comment makes sense (otherwise ReadA should 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.

  4. Andarist commented on Jan 20, 2026

    @Andarist
    Contributor

    Matt 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 inferring R | undefined into R. I suspect that creates the issue later on - given that inference has ReturnType priority 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 inferToMultipleTypes can't match up discriminated unions like this to infer between "matching" types.

  5. Ichi075 commented on Sep 13, 2026

    @Ichi075

    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, causing R | undefined to 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Domain: check: Type InferenceRelated to type inference performed during signature resolution or `infer` type resolutionHelp WantedYou can do thisPossible ImprovementThe current behavior isn't wrong, but it's possible to see that it might be better in some cases

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions