Why is TS not complaining here:
function print<G>(params: G[]): G {
return params[0];
}
console.log(print(["1",false,"2",5]))
Array elements have different types no?
Why is TS not complaining here:
function print<G>(params: G[]): G {
return params[0];
}
console.log(print(["1",false,"2",5]))
Array elements have different types no?
TypeScript wouldn't complain because you're using generics correctly for its intended purpose, called type argument inference:
Here we use type argument inference — that is, we want the compiler to set the value of Type for us automatically based on the type of the argument we pass in:
let output = identity("myString");
Notice that we didn’t have to explicitly pass the type in the angle brackets (
<>
); the compiler just looked at the value "myString" [...]. While type argument inference can be a helpful tool to keep code shorter and more readable [...]
In this case, the inferred type of G
will be based on the type of the array you are passing to the function.
["1", false, "2", 5]
will have an inferred type of Array<string | boolean | number>
, so G
will have the inferred union type of string | boolean | number
. And that is exactly what params[0]
is returning, which matches the explicit return type you've defined.
We'll be using IsUnion
from this answer, along with UnionToIntersection
from here. Those answers already have excellent explanations, so if you're interested, make sure to read them.
The only magic I do here is using IsUnion
to check if G
is a union:
function print<G>(params: readonly (IsUnion<G> extends true ? never : G)[]): G {
return params[0];
}
I use readonly
so you can pass readonly and regular arrays (since regular arrays are assignable to readonly arrays, but not the other way around). Then there is IsUnion<G> extends true ? never : G
.
TypeScript is actually able to infer the type of G
here, so it can pass it to IsUnion
. If G
was a union type, we disallow that and replace it with never
.
Here's a playground with some cases. Note that this will error for types like 1 | 2 | 3
since that's still a union.
What @Terry noted is correct. TypeScript's Heuristics will infer G
to be a union type of all the elements in the array. If you don't want this behaviour, this might be helpful:
function print2<G>(params: [G, ...(G & {})[]]): G {
return params[0];
}
print2(["1","2"]) // Valid
print2(["1",false,"2",5]) // Error
Here, G
will be inferred as the type of the first element in the tuple. The rest of the elements must comply to this type.
We give params
a tuple type. The first element of this tuple is just G
and TypeScript can use this element to infer G
. The rest of the tuple... well it's an potentially unlimited amount G
s too, but we intersect them with {}
. This is a common way of telling TypeScript "Hey, please just take the type of G
here, but don't use this position to infer it". You can see a discussion about that here.