Skip to content
Theory · Generics · lesson 5 of 10

Constraints with extends

Sometimes it is not enough for a generic function to know that T is some type: it needs something from the value itself. Let's write a function longest that returns the longer of two values, strings or arrays:

TypeScript
function longest<T>(a: T, b: T): T {
    return a.length >= b.length ? a : b; // ✗
}
TEXT
error TS2339: Property 'length' does not exist on type 'T'.

TypeScript is right. Any type may be put in place of T: number, boolean, even null. A number has no length, and the function would break on the first such call. While nothing is known about T, TypeScript does not let you read properties of the value.

Let's go back to the port. A crane lifts any container that has lifting lugs on its roof for the hooks. The crane does not care what is inside: furniture or bananas. But it will not take a container without lugs. The function longest needs the same: any cargo, as long as it has a length property.

A constraint with extends

A constraint on a type parameter is written with the word extends after its name. It says which types may be put in place of T:

TypeScript
function longest<T extends { length: number }>(a: T, b: T): T {
    return a.length >= b.length ? a : b;
}

console.log(longest("cat", "horse")); // horse
console.log(longest([1, 2], [3]).join(" ")); // 1 2

Let's take T extends { length: number } apart:

  • T is the type parameter, as before;
  • extends means "fits": only a type that fits the type on the right may be put in;
  • { length: number } is the requirement: the value must have a length property holding a number.

What this gives:

  1. Inside the function TypeScript knows that a and b have length, and the error is gone.
  2. longest("cat", "horse") passes: strings have length. The program prints horse.
  3. longest([1, 2], [3]) passes too: arrays have length. The program prints 1 2.

You have already seen the word extends on classes. Here it is not about inheritance: a string inherits nothing from { length: number }, it has the needed property, and that is enough. TypeScript compares types by shape, as with interfaces.

An argument without length

Now let's pass numbers:

TypeScript
function longest<T extends { length: number }>(a: T, b: T): T {
    return a.length >= b.length ? a : b;
}

console.log(longest(10, 200)); // ✗
TEXT
error TS2345: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

tsc compares the argument with the constraint, and number does not fit { length: number }. Notice where the error is: on the line of the call, not in the function's declaration. The function is written correctly; the call that broke its requirement is wrong. This is the container without lugs: the crane was stopped before the lift.

Here the picture ends. The crane operator looks at the container itself, while tsc looks only at the value's type. It will not let through a value typed unknown, even if it actually holds a string.

Why T, if there is a parameter type

Could you do without T and write the requirement right in the parameters: function longest(a: { length: number }, b: { length: number })? The function takes the same arguments, but its result has the type { length: number }. TypeScript forgets that strings came in, and longest("cat", "horse").toUpperCase() does not compile:

TEXT
error TS2339: Property 'toUpperCase' does not exist on type '{ length: number; }'.

With T extends { length: number } TypeScript remembers that strings came in: the result has a string type, and toUpperCase() compiles. The constraint says what the value must be able to do, and T remembers what it was.

Interviewers often ask what extends does in <T extends …>. The answer: it limits which types may be put in place of T, and lets the function use what the constraint promises.

Common mistake

Inside the function, the code uses something the constraint does not promise:

TypeScript
function shout<T extends { length: number }>(x: T): string {
    return x.toUpperCase(); // ✗
}
TEXT
error TS2339: Property 'toUpperCase' does not exist on type 'T'.

The constraint promises only length. An array may arrive in place of T, and an array has no toUpperCase. Inside the function you get exactly what the constraint says. If the function needs string methods, it does not need T: its parameter is an ordinary string.

Key points

  • T extends { length: number } lets only a type with a length property take the place of T: strings and arrays fit, numbers do not.
  • Inside the function you may use what the constraint promises, and nothing more.
  • tsc marks an argument that does not fit the constraint on the line of the call: error TS2345.
  • Unlike the type { length: number } on a parameter, T keeps the exact type of the result.