How T is inferred
In the last lesson TypeScript worked out by itself that first([100, 250]) returns number | undefined, although nobody wrote first<number>. Where does it get the type for T?
Let's go back to the port. The label in the holder on a container's door is written by the loader: they see what goes inside and fill in the holder. When you call a generic function, TypeScript works as the loader. It looks at the arguments and decides by itself which type to put in place of T. Each call is a new loading with a new label.
The type from the arguments
When you call first([100, 250]), TypeScript matches the argument against the parameter:
- The parameter is declared as
xs: T[], an array of elements of typeT. - The argument
[100, 250]is an array of numbers,number[]. - For
number[]to matchT[],numbermust stand in place ofT. This is type argument inference: TypeScript findsTfrom the types of the arguments.
Inference starts over at every call. One call does not affect the next: if you then call first(["Paris", "Rome"]), T becomes string, while the first call keeps number. That is why one function returns the exact type for different inputs.
If the array holds values of different types, T gets their union. The literal [1, "a"] has the type (string | number)[], so T is string | number.
Step through the program. Watch the hover on the variables that hold the results: it shows what T became in that call.
1function first<T>(xs: T[]): T | undefined {editor hoverxs: T[]2 return xs[0];3}4 5const price = first([100, 250]);6const city = first(["Paris", "Rome"]);7const mixed = first([1, "a"]);8const exact = first<number>([1, "2"]);9 10function createList<T>(): T[] {11 return [];12}13 14const items = createList();15const nums = createList<number>();
Inside the declaration T is an empty label holder: the call decides which type goes there.
Two type parameters
A function may have several type parameters. TypeScript infers each one from its own argument, independently of the others:
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const entry = pair("Ann", 30);
console.log(entry[0], entry[1]); // Ann 30Let's see what happens here:
<A, B>declares two type parameters, separated by a comma.- From
"Ann"TypeScript infersAasstring, and from30it infersBasnumber. - The result is declared as the tuple
[A, B], soentryhas the type[string, number]. console.logprintsAnn 30.
When there is nothing to infer from
The function createList from the example above has no parameters, and T appears nowhere among the arguments. When the result goes into a variable with no annotation, there is nothing to fill the holder with, so TypeScript puts unknown in it:
function createList<T>(): T[] {
return [];
}
const items = createList();
const nums = createList<number>();
nums.push(5);
console.log(items.length, nums.length); // 0 1items has the type unknown[]. You can put anything into such an array, but you cannot use an element without checking it. For nums the type argument is written explicitly, and it is an ordinary number[]: only numbers go into it. An annotation on the variable gives the same type: in const ids: number[] = createList() TypeScript takes T from where the result goes.
Here the loader picture ends. A real loader would look into an empty container and ask what will be put in it later. TypeScript decides everything at the moment of the call: the lines after it do not affect T. If you write items.push(5) below, items still has the type unknown[].
Interviewers often ask how TypeScript chooses T when you call a generic function without angle brackets. The answer: it infers it from the arguments of that call; when they lack T, from the annotation of the variable taking the result; with neither, T becomes unknown.
Common mistake
Values of different types are passed to a function with one T for two parameters:
function both<T>(a: T, b: T): T[] {
return [a, b];
}
console.log(both(1, "a")); // ✗error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.It seems that T should become string | number, as for the array [1, "a"]. But T is one type for the whole call: TypeScript took number from the first argument, and the string does not fit it. Inside one array literal TypeScript unites the types of the elements, but for two separate arguments of one T it does not. If the function must take both types, say so explicitly: both<string | number>(1, "a").
Key points
- TypeScript infers
Tfrom the arguments of a call:first([100, 250])givesnumber | undefined, andfirst(["Paris"])givesstring | undefined. - Inference starts over at every call, so one function returns the exact type for different inputs.
- Several type parameters are inferred independently:
pair("Ann", 30)gives[string, number]. - If there is nothing to infer
Tfrom, neither the arguments nor the variable's annotation, it becomesunknown; then the type argument is written explicitly:createList<number>().