Skip to content

Type argument incorrectly inferred from union type #56792

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
OliverJAsh opened this issue Dec 15, 2023 · 7 comments
Closed

Type argument incorrectly inferred from union type #56792

OliverJAsh opened this issue Dec 15, 2023 · 7 comments
Labels
Duplicate An existing issue was already created

Comments

@OliverJAsh
Copy link
Contributor

OliverJAsh commented Dec 15, 2023

πŸ”Ž 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);
@Andarist
Copy link
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.

@jcalz
Copy link
Contributor

jcalz commented Dec 15, 2023

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

(also see #19596 and SO question)

@OliverJAsh
Copy link
Contributor Author

OliverJAsh commented Dec 15, 2023

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
);

@fatcerberus
Copy link

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.

@fatcerberus
Copy link

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

@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.

@craigphicks
Copy link

craigphicks commented Dec 17, 2023

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.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Dec 20, 2023
@typescript-bot
Copy link
Collaborator

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

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Dec 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

7 participants