Home > Software design >  Why doesn't generic type inference infer a union from two sites?
Why doesn't generic type inference infer a union from two sites?

Time:12-23

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

CodePudding user response:

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

CodePudding user response:

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.

  • Related