Skip to content

Type argument incorrectly inferred from union typeΒ #56792

Closed as not planned
Closed as not planned

Description

πŸ”Ž Search Terms

  • generics
  • type argument
  • inference
  • union type

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about type parameters/arguments and inference.

⏯ Playground Link

https://www.typescriptlang.org/play?#code/CYUwxgNghgTiAEYD2A7AzgF3gMwFzwB4BBAPgAoN8BtIgXQEp4BeE+ANyQEtgBuAKFCRYCZOiwAPaphicUAc1rwAPvCooArgFsARiBi1+fbGXH0eQA

πŸ’» Code

declare const f: <A>(t: [A]) => void;
declare const x: [string] | [number];

f(x);

πŸ™ Actual behavior

TypeScript infers the type argument as string.

πŸ™‚ Expected behavior

TypeScript infers the type argument as string | number.

Additional information about the issue

This issue appears to describe a similar problem with functions: #52295.

Reduced test case:

type A = { a: string };
type B = { b: string };

// Parameter types
declare const f: <A>(ff: (x: A) => void) => void;
declare const x: ((x: A) => void) | ((x: B) => void);
// Inferred type argument: A
// Expected: A & B
f(x);

// Return types
declare const g: <A>(gg: () => A) => void;
declare const y: (() => A) | (() => B);
// Inferred type argument: A
// Expected: A | B
g(y);

Activity

  1. Andarist commented on Dec 15, 2023

    @Andarist
    Contributor

    The difference is that in those other examples the type parameter appears in what it considers a naked~ position. It unionify candidates from such positions.

    The union of tuples is different since the position is not naked, it’s an element inside a tuple type.

    This isn’t incorrect. How different inferences work is often just heuristic-based. As far as I know, this is working as intended - it’s not a bug. It can be treated as a feature request though.

  2. jcalz commented on Dec 15, 2023

    @jcalz
    Contributor

    If it's a feature request then it duplicates #44312

    (also see #19596 and SO question)

  3. OliverJAsh commented on Dec 15, 2023

    @OliverJAsh
    ContributorAuthor

    Thanks for the responses. If it provides more context, below is the original real world problem I encountered before reducing it down to the examples you see above. I've extracted these types from the popular library fp-ts.

    export interface Reader<R, A> {
      (r: R): A;
    }
    
    declare const chain: <A, R, B>(
      f: (a: A) => Reader<R, B>
    ) => (ma: Reader<R, A>) => Reader<R, B>;
    
    declare const boolean: boolean;
    declare const a: Reader<string, number>;
    declare const b: Reader<number, number>;
    
    // Type argument `R` inferred as `string`.
    // Ideally this would be `string | number`.
    chain(() =>
      // Error: Type 'string' is not assignable to type 'number'.
      boolean ? a : b
    );

    and

    interface Left<E> {
      readonly _tag: "Left";
      readonly left: E;
    }
    
    interface Right<A> {
      readonly _tag: "Right";
      readonly right: A;
    }
    
    export type Either<E, A> = Left<E> | Right<A>;
    
    declare const chain: <E, A, B>(
      f: (a: A) => Either<E, B>
    ) => (ma: Either<E, A>) => Either<E, B>;
    
    declare const boolean: boolean;
    declare const a: Either<string, string>;
    declare const b: Either<string, number>;
    
    // Type argument `B` inferred as `string`.
    // Ideally this would be `string | number`.
    chain(() =>
      // Error: Type 'number' is not assignable to type 'string'.
      boolean ? a : b
    );
  4. fatcerberus commented on Dec 16, 2023

    @fatcerberus

    There’s also the fact to consider that, in general, G<T | U> cannot be safely substituted for G<T> | G<U> for arbitrary G, so TS won’t even attempt inferences that imply such a substitution.

  5. fatcerberus commented on Dec 17, 2023

    @fatcerberus

    The description "Actual Behavior" doesn't match the actual behavior

    Craig P Hicks (@craigphicks) The OP claims that Actual Behavior is:

    TypeScript infers the type argument as string.

    which is consistent with the error message:

    Argument of type '[string] | [number]' is not assignable to parameter of type '[string]'.

    i.e. TS infers A = string for the call to f and subsequently produces an error upon trying to pass a [string] | [number] (that is, x) to a parameter typed as [A] = [string]. So the description of actual behavior looks correct to me.

  6. craigphicks commented on Dec 17, 2023

    @craigphicks

    The definition can be modified to make it pass without inferring a union, while still requiring the argument to be a unary-tuple.

    declare const f: <A extends [unknown]>(t: A) => void;
    declare const x: [string] | [number];
    f(x); // <[string] | [number]>(t: [string] | [number]) => void    NO ERROR
    

    That is a workaround if the function declaration can be modified (or the function declaration could be coerced). Is there any semantic difference between

    declare const f: <A>(t: [A]) => void;
    

    and

    declare const f: <A extends [unknown]>(t: A) => void;
    

    ? If no semantic difference, then it could be argued that they should infer in exactly the same way, and that therefore this issue is a bug, even if it is a design bug.

    (Maybe there are some other test cases that former passes that the latter doesn't?)

    Here is another example of what seem to be semantically identical generics where one has trouble.

  7. typescript-bot commented on Dec 23, 2023

    @typescript-bot
    Contributor

    This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  8. locked as resolved and limited conversation to collaborators on Oct 22, 2025
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

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions