1

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?

  • 2
    Why would it complain? You are passing in a generic, which means it infers `G` based on the array provided, which is in this case `string | boolean | number`. It then returns the inferred union type as expected. What kind of error do you expect? – Terry Sep 29 '22 at 18:43
  • If you want to make it complains - add `as const` like this `["1",false,"2",5] as const` – Drag13 Sep 29 '22 at 18:44
  • @Terry AAh so the type of G it inferred as that: `string | boolean | number`? interesting. post as answer if you like I will accept –  Sep 29 '22 at 18:45
  • @Drag13 Can you explain why would it complain in that case? I tried but I think it is mainly complaining about mutable vs readonly now –  Sep 29 '22 at 18:45
  • 1
    @Drag13 That only works because they used `G[]`, not `readonly G[]`. If they use `readonly G[]` then that won't error. – kelsny Sep 29 '22 at 18:47
  • A workaround if you're interested: https://tsplay.dev/w610rW This will break if you use a type like `1 | 2 | 3` still of course, but it just makes sure that the inferred type is a single type. – kelsny Sep 29 '22 at 18:49
  • @caTS I am TS beginner I think that is too much for me. But if you want you can add it as an answer too, but with explanations, because I couldn't follow it. Should be useful for someone else too. I will upvote at least –  Sep 29 '22 at 18:53

3 Answers3

1

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.

Terry
  • 63,248
  • 15
  • 96
  • 118
  • yeah I was mainly surprised why it accepted params of different type not about the return. But yeah this answers. I will accept soon. –  Sep 29 '22 at 18:48
1

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.

kelsny
  • 23,009
  • 3
  • 19
  • 48
  • Yeah I suppose I will first have to read at least those two links, and then try to come back here. Like I said I am TS beginner, and here are too many new concepts for me. Thanks –  Sep 29 '22 at 19:00
0

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


Playground

Tobias S.
  • 21,159
  • 4
  • 27
  • 45