Skip to content
Theory · Generics · lesson 3 of 10

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:

  1. The parameter is declared as xs: T[], an array of elements of type T.
  2. The argument [100, 250] is an array of numbers, number[].
  3. For number[] to match T[], number must stand in place of T. This is type argument inference: TypeScript finds T from 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.

LiveWhat TypeScript put in place of Tstep 1 of 7

Inside the declaration T is an empty label holder: the call decides which type goes there.

Press "Next step" and watch what T became at each call

Two type parameters

A function may have several type parameters. TypeScript infers each one from its own argument, independently of the others:

TypeScript
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 30

Let's see what happens here:

  1. <A, B> declares two type parameters, separated by a comma.
  2. From "Ann" TypeScript infers A as string, and from 30 it infers B as number.
  3. The result is declared as the tuple [A, B], so entry has the type [string, number].
  4. console.log prints Ann 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:

TypeScript
function createList<T>(): T[] {
    return [];
}

const items = createList();
const nums = createList<number>();
nums.push(5);
console.log(items.length, nums.length); // 0 1

items 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:

TypeScript
function both<T>(a: T, b: T): T[] {
    return [a, b];
}

console.log(both(1, "a")); // ✗
TEXT
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 T from the arguments of a call: first([100, 250]) gives number | undefined, and first(["Paris"]) gives string | 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 T from, neither the arguments nor the variable's annotation, it becomes unknown; then the type argument is written explicitly: createList<number>().