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:
function longest<T>(a: T, b: T): T {
return a.length >= b.length ? a : b; // ✗
}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:
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 2Let's take T extends { length: number } apart:
Tis the type parameter, as before;extendsmeans "fits": only a type that fits the type on the right may be put in;{ length: number }is the requirement: the value must have alengthproperty holding a number.
What this gives:
- Inside the function TypeScript knows that
aandbhavelength, and the error is gone. longest("cat", "horse")passes: strings havelength. The program printshorse.longest([1, 2], [3])passes too: arrays havelength. The program prints1 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:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
console.log(longest(10, 200)); // ✗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:
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:
function shout<T extends { length: number }>(x: T): string {
return x.toUpperCase(); // ✗
}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 alengthproperty take the place ofT: 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,Tkeeps the exact type of the result.