Skip to content
Theory · Generics · lesson 1 of 10

Generic functions

What you'll learn

What you'll learn:

  • write generic functions that keep the exact type of a value;
  • understand how TypeScript infers the type parameter T from the arguments of each call;
  • limit a type parameter with extends;
  • describe generic types and classes, and read Array<T>, Map<K, V> and Set<T>.
Three identical containers with label holders, one marked T and two loaded as number and string

Picture a seaport. All its containers are the same: steel walls, doors, a lock. One is loaded with furniture, another with bananas, a third with books. One design serves any cargo, and to show what is inside, each door has a label holder. It is filled in at loading, and a port on the other side of the world knows what is inside without opening the doors.

Functions can be containers like that. The function first returns the first element of an array, and it does not care what the array holds. In JavaScript you write it once. How do you describe its types in TypeScript?

The first thing that comes to mind is the any type. There is one function and it takes any array, but the label has been torn off the container:

TypeScript
function first(xs: any[]): any {
    return xs[0];
}

const price = first([100, 250]);
console.log(price.toUpperCase()); // ✗
TEXT
TypeError: price.toUpperCase is not a function

tsc stays silent: price has the type any, and checking is off. Only whoever runs the program sees the error. The second way is a copy of the function per type: firstNumber, firstString and so on. Checking works, but you write the same body again and again.

The type parameter T

A generic function declares a type parameter in angle brackets after its name: a name like T that a real type replaces at every call. This is the label holder:

TypeScript
function first<T>(xs: T[]): T | undefined {
    return xs[0];
}

const price = first([100, 250]);
const city = first(["Paris", "Rome"]);
console.log(price, city); // 100 Paris

Let's take the declaration apart:

  • <T> after the name declares the type parameter T;
  • xs: T[] is the function's parameter: an array whose elements are all of type T;
  • T | undefined is the result type: an element of the same type T, or undefined for an empty array.

Let's see what happens at the calls:

  1. first([100, 250]) gets an array of numbers, and TypeScript fills in the label: T becomes number. The editor's hover on price shows number | undefined.
  2. first(["Paris", "Rome"]) gets an array of strings, and here T is string. city has the type string | undefined.
  3. console.log prints 100 Paris.

There is one function, and each call gets its own result type. And checking is back:

TypeScript
function first<T>(xs: T[]): T | undefined {
    return xs[0];
}

const price = first([100, 250]);
console.log(price?.toUpperCase()); // ✗
TEXT
error TS2339: Property 'toUpperCase' does not exist on type 'number'.

tsc knows that price holds a number or undefined, and finds the mistake before the program runs.

Interviewers often ask why you need generics and how they are better than any. The answer: a generic function works with any type and still links the input type to the result type, while any breaks that link and turns checking off.

An explicit type argument

The type put in place of T is called a type argument. TypeScript usually finds it by itself, but you can also write it explicitly, in angle brackets at the call: first<number>([100, 250]). Then TypeScript does not infer T; it checks the arguments against the given type. The call first<number>([1, "2"]) does not compile:

TEXT
error TS2322: Type 'string' is not assignable to type 'number'.

It is like a label written before loading: cargo that does not match it is not let into the container.

Here the port picture ends. In a port the label travels with the container, while T, like every type, exists only for tsc. In the JavaScript file the function becomes function first(xs), with no angle brackets.

Common mistake

A generic function returns a value of a concrete type:

TypeScript
function firstOr<T>(xs: T[]): T {
    if (xs.length === 0) {
        return 0; // ✗
    }
    return xs[0];
}
TEXT
error TS2322: Type 'number' is not assignable to type 'T'.
  'T' could be instantiated with an arbitrary type which could be unrelated to 'number'.

The second line of the message gives the reason: any type unrelated to number may be put in place of T. T is chosen by whoever calls the function, not by the function itself. The call firstOr(["Paris"]) expects a string, and 0 would deceive it. It is like loading bananas in advance into a container whose label the sender will fill in.

A fallback value is passed as a parameter of the same type: function firstOr<T>(xs: T[], fallback: T): T. Then the calling code chooses it, and the types match.

Key points

  • A generic function declares a type parameter <T>, and at every call a real type takes the place of T.
  • first<T>(xs: T[]): T | undefined links the element type to the result type: for an array of numbers the result is number | undefined.
  • any also accepts any array, but it loses the result type and turns checking off.
  • A type argument can be written explicitly, first<number>(…), and then TypeScript checks the arguments against it.