Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript generics will only infer union types in simple cases

Tags:

typescript

Here's a code example:

declare function test_ok<T>(arr: T[]): T;
test_ok([1, 2, "hello"]); // OK
test_ok([[1], [2], ["hello"]]); // OK

// note the nested array signature
declare function test_err<T>(arr: T[][]): T;
test_err([[1], [2], ["hello"]]); // ERR type "string" is not assignable to type "number"
test_err<string | number>([[1], [2], ["hello"]]); // OK if generic is specified

It seems that the general case, TypeScript is able to infer the best common type (a basic union) when given a heterogeneous array. However, if you try to scope the generic any further than just a simple array (such as the nested array above), it gives up. I've also found this with other cases (e.g. an array of functions where the generic is over the return type of the functions, rather than entire functions). Is this some sort of performance optimization?

like image 641
y2bd Avatar asked Sep 16 '19 00:09

y2bd


1 Answers

I'm going to say this is more like intentional error-catching and not a performance optimization. When trying to infer T given a set of values that are supposed to be of type T, you can always succeed by widening T to fit all values, but then it's impossible to catch a legitimate mistake, where one of the values was entered incorrectly. This is a judgment call by the designers of the language, I think, and likely any heuristic would produce some false positives and some false negatives.

The GitHub issue microsoft/TypeScript#31617 is a similar report, where a user expects string | number to be inferred from two arguments, one of type string and the other of type number. The response from one of the language maintainers is:

This is an intentional trade-off so that generics can catch errors where you expect multiple objects to be of the same type, which is usually more common.


So, what can be done? Obviously you can manually specify T as string | number. Otherwise, if you want your function to be permissive and never throw an error, you can make T the type of the given argument itself. The compiler is much more consistent about inferring a type given an value of that type than it is when given a value of some function of that type. So my workaround here would be this for arrays:

declare function test_fixed<T extends any[][]>(arr: T): T[number][number];
const ret = test_fixed([[1], [2], ["hello"]]); // string | number

In this case, T is constrained to be a doubly-nested array, and the return type, T[number][number] is the element type of the innermost array (if you take T, and look up a value at a number index, you will get another array type... if you look up a value of that at a number index, you get the innermost element type... T[number][number].)

Okay, hope that helps. Good luck!

Link to code

like image 98
jcalz Avatar answered Sep 23 '22 23:09

jcalz