6

Consider

const pairer = <T,>(a: T, b:T) => ({a, b})
const mypair = pairer(3n, true)

This is an error in TypeScript 4.9, namely

Argument of type boolean is not assignable to parameter of type bigint

So clearly TS has inferred T to be bigint in the call, making the call invalid. But there is a perfectly good type for T that makes the call legal, namely bigint | boolean:

const myotherpair = pairer<bigint | boolean>(3n, true)

compiles and runs fine.

Why doesn't TypeScript infer the type that makes the code it is processing valid? Is there a concise writeup of the inference of a generic parameter from an actual call with enough detail to understand why it chose bigint in the mypair call? (The TypeScript handbook page on generics doesn't cover cases with two uses of the parameter.) And perhaps most importantly, is there a way for me to define my generic so that the unadorned call mypair(x,y) will infer the type (typeof x) | (typeof y)?

Ken White
  • 123,280
  • 14
  • 225
  • 444
Glen Whitney
  • 446
  • 2
  • 12
  • "And perhaps most importantly, is there a way for me to define my generic so that the unadorned call mypair(x,y) will infer the type (typeof x) | (typeof y)?" There should only be one main question per post; is this the primary question (in which case the "why" question is not one) or do you want to move it to a new post? – jcalz Dec 22 '22 at 22:37
  • The way to define this would be write `(a: T, b: U): { a: T | U, b: T | U } => ({ a, b })` as shown [here](https://tsplay.dev/N5OQ9W). Does that meet your needs? If so I could write up an answer explaining how it works and why it's necessary to do this (assuming this was indeed your primary question). If not, what am I missing? – jcalz Dec 22 '22 at 22:41
  • Yes that's really the primary question, thanks, and indeed, that declaration works like a charm in this case. The syntax sort of obscures the semantics that we're really thinking of a and b as the same type, just a type big enough to contain them both, but as it does precisely what's desired, we can just comment it. So sure, an answer would be welcome. – Glen Whitney Dec 23 '22 at 00:09

2 Answers2

2

I am assuming the following is the primary question:

Is there a way for me to define my generic so that the unadorned call mypair(x,y) will infer the type (typeof x) | (typeof y)

As you saw, you can't do this with a single generic type parameter T. The compiler does not like to synthesize unions from multiple inference sites, as described in Why isn't the type argument inferred as a union type?.

If you want to emulate this behavior, you can provide multiple type parameters, and then synthesize their union yourself:

const pairer = <T, U>(a: T, b: U): { a: T | U, b: T | U } => ({ a, b });

In the above, T can be inferred only from a, and U can be inferred only from b. They are independent. The return type, on the other hand, is annotated as { a: T | U, b: T | U }, to provide the same output as you would get if both a and b were used to infer a single type argument.

Note that if you don't annotate the return type, the compiler will infer the more specific type {a: T, b: U}. Presumably you actually prefer the wider version where both a and b are of the same type, though.

You can see it in action:

const mypair = pairer(3n, true);
/* const mypair: {
    a: bigint | boolean;
    b: bigint | boolean;
} */

Looks good. There is no complaint at the call site, and the type of mypair has bigint | boolean members as desired.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
0

To me this is fairly simple, it's the least surprising behaviour :

You'd expect the compiler to warn you when you pass 2 different types. When can see that as a feature that you can bypass this warning by setting the type as a union.

This works as intended per #37673, but you can always an open issue (#44312) asking for that particular feature to allow the inference to be widden by the parameters.

Matthieu Riegler
  • 31,918
  • 20
  • 95
  • 134
  • Hmm, it's hard for me to know what one means by "two different types" in TypeScript. There's just a type system, and it allows for abitrary meets and joins of types. The actual occurrence of this came up in the guts of a fairly complicated generic, and the actual types that were coming in as the two parameters were already overlapping unions of other types, so actually I found the asymmetry of the two argument positions more surprising than the compiler just combining the two into one big union. So I guess "surprising" is very context-dependent. – Glen Whitney Dec 22 '22 at 20:40
  • So I guess the important things are to _understand_ what TypeScript is really doing when it infers types, and to be able to _control_ it so that when I want to make something like this that will generate unions I can (if possible). – Glen Whitney Dec 22 '22 at 20:41
  • Btw I edited my answer. There is a year old issue asking for the same feature as you : https://github.com/microsoft/TypeScript/issues/44312 – Matthieu Riegler Dec 22 '22 at 21:34
  • Thanks for the additional info. I am gathering from the issues that there is not currently a direct way to define a generic like this that automatically infers the union of its parameters. So I guess the last piece is a good reference on what TypeScript actually does, if such a thing exists. – Glen Whitney Dec 22 '22 at 22:18