Topic 7.3
Generic Constraints
In one line
A constraint (T extends Something) limits which types may fill a blank, and in return the function body can use everything the constraint promises, while still returning the caller's exact type.
Think of it like this
A ride at a fair with a sign: "You must be at least this tall." Anyone can walk up, but only people who pass the test get on. The ride does not care if you are a child, a teenager or a tall grandparent. It only checks the one thing it relies on. A constraint is that sign. It says the type can be anything, as long as it has the one thing the function needs.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Constraint
- A rule on a type parameter that limits which types may fill it, written
T extends Something. - extends (in a constraint)
- "Is assignable to" or "has at least this shape". It isn't class inheritance here.
- keyof T
- A union of the property names of
T, such as"name" | "age". - Indexed access type
T[K]: the type of propertyKon typeT.- Subtype
- A type that has everything another type has, and maybe more.
{ id: number; label: string }is a subtype of{ id: number }. - Preserve
- To return the caller's exact type instead of a simpler one, so no information is lost.
- Intersection
A & B: a type that has all the members of both.- Implicit constraint
- The one used when you write none:
unknown. It allows nothing special.
Step by step
01Without a constraint, no .length
T could be a number, a boolean, anything. The compiler can't know that it has a length, so reading it is an error.
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'.
}02Add the sign: T extends { length: number }
Now every possible T is promised to have a numeric length. The body type-checks. The callers must prove it: strings, arrays and anything with a length pass, numbers don't.
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("apple", "fig"); // string-like
longest([1, 2, 3], [4]); // number[]
longest(10, 20); // error: number has no length03The result keeps its real type
longest returns T, not { length: number }. For two arrays the result is number[], so .push, .map and the rest are available. The constraint only checks the input; the output keeps the caller's type.
04Link two type parameters with keyof
K extends keyof T ties the key to the object. T is the object type, K is one of its key names, and the return type T[K] looks the property up.
A wrong key isn't just a failed lookup: it is rejected before the call, and the error lists the real keys.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Asha", age: 30 };
getProperty(user, "age"); // number
getProperty(user, "email"); // error TS234505Constraints narrow like any other type
A constraint of string | number lets you narrow with typeof inside the function, exactly as with a union parameter (Topic 4.3). The difference is the return type: you can keep the caller's exact type.
A function that must produce a new T can't, because only the caller knows what T really is. Spread the value you were given ({ ...item, id: item.id + 1 }) instead of building one from scratch.
Try it yourself
- 1
Pass something with a length
In the first example add
longest({ length: 3, tag: "mine" }, { length: 9, tag: "yours" })and ask// ^?. The object passes because it has a numericlength, and the result keeps itstag. - 2
Mix the types
Try
longest("apple", [1, 2]). Predict first: dostringandnumber[]both satisfy the constraint? Yes, butThas to be one type for both arguments. The compiler fixesTfrom the first argument and complains about the second:Argument of type 'number[]' is not assignable to parameter of type '"apple"'. (Topic 7.6 explains.) - 3
Misspell a key
In the second example change
"age"to"agee". The message lists the real keys: this is how editors can suggest property names for you in the call.
Code & diagrams
Strings and arrays both have a length. The result for arrays is number[] (not just { length: number }), so array methods are still there. A number is rejected with the constraint in the message.
Console
(nothing was printed)Expected console output
apple [ 1, 2, 3 ]The error lists the allowed keys. For "age" the result type is number, for "name" it is string: T[K] is looked up per call. Try getProperty(user, "adm") and read the message.
Console
(nothing was printed)Expected console output
Asha 30touch works for any shape that has an id and keeps the extra label. withDefaults tries to create a T out of nothing, which the compiler refuses: the caller picks T and could demand more properties than the function can provide.
Console
(nothing was printed)Expected console output
{ id: 2, label: 'kept' }Inside describe, typeof narrows T the same way it narrows a union. merge returns the intersection of both inputs. The last two lines are errors: true is not string | number, and an unconstrained T has no length. The code still runs and describe(true) fails at runtime, which is the bug the error predicted.
Console
(nothing was printed)Expected console output
HI 2.0 { a: 1, b: 'x' }
Uncaught TypeError: value.toFixed is not a functionBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Remove the constraint
In the last example, unsafe<T>(x: T) reads x.length. Notice there is no extends.
Break #2
Build a T from nothing
In withDefaults, the line const copy: T = { id: 0 }; tries to create a value of the caller's type.
Myth vs fact
Myth
T extends X means T inherits from class X.
Fact
In a constraint, extends is about shape: "is assignable to". An object literal, an interface, an array or a string can all satisfy T extends { length: number }, with no classes involved.
Myth
A constrained generic returns the constraint type.
Fact
It returns whatever T is at the call site. longest([1, 2], [3]) is number[], not { length: number }. That is the point of a constraint over a plain parameter type.
Myth
K extends keyof T just means K is a string.
Fact
keyof T is the exact union of T's keys, so K is limited to those names and the result type T[K] can be worked out per call. Keys can also be numbers or symbols (Topic 8.1).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Inside the function,
T's base constraint is used for property access and assignment checks.value.lengthworks because the compiler resolvesTto{ length: number }for that expression, butTitself remains opaque. That is why you can read from aT extends HasIdbut cannot assign a new object to it. - ▸
Constraints are checked at each call, with the inferred type argument, and the error appears on the argument that caused the failure when the compiler can tell which one it was. If you want a better message, a conditional type (Phase 9) can produce a custom error shape; a constraint alone gives the generic wording.
- ▸
The
K extends keyof Tpattern is the base of the standard library'sPick<T, K extends keyof T>, andRecord<K extends keyof any, T>uses the same idea for a key set (Topic 10.1). Writing apluckyourself first makes those definitions easy to read.
Remember this
- 1
The problem this solves: An unconstrained generic could be anything, so you can't read
.lengthor any property inside it. A constraint states the minimum the type must have. In Topic 7.1 you saw that insidefunction f<T>(x: T)you can't do anything withx, becauseTcould be anything. That is too strict for real code: a function that returns the longer of two things needs.length. A constraint fixes this:function longest<T extends { length: number }>(a: T, b: T): T. Readextendshere as "is assignable to" (a value of that type may be used in that spot, Topic 3.5) or "has at least this shape". Nowa.lengthis allowed, because everyTis promised to have one. - 2
What may fill the blank? Any type that has what the constraint asks for. A
stringhaslength. An array haslength. An object{ length: 3, tag: "x" }has it. Anumberdoes not, solongest(10, 20)is rejected with an error that names the constraint. The constraint acts on the type argument, so extra properties on the real type are fine. - 3
Why not write
function longest(a: { length: number }, b: { length: number }): { length: number }? Because the return type would then be just{ length: number }: you would get back something that has a length and nothing else. WithT extends ...the function returns the same type it received,stringfor strings,number[]for arrays, and you keep every method and property. That's the difference between "accepts anything with alength" and "accepts anything with alengthand remembers what it was". - 4
The most useful constraint relates two type parameters.
K extends keyof Tmeans "Kmust be one of the property names ofT".keyof Tis the union of the keys ofT(Topic 8.1 studies it fully). Withfunction getProperty<T, K extends keyof T>(obj: T, key: K): T[K], the key must exist on the object, and the return typeT[K]is the type of that property:getProperty(user, "age")isnumber, while"email"is an error.T[K]is an indexed access type (Topic 8.3). This single signature is how safepluck,get,pickandsortByhelpers are written. - 5
Constraints can be unions (
T extends string | number), object types, interfaces, classes (T extends Animal), another type parameter (U extends T), or a function type. Several constraints combine with&(T extends HasId & HasName). Inside the body, the compiler uses the constraint as the best known type ofT, so after narrowing aT extends string | numberwithtypeof value === "string"you can callvalue.toUpperCase(). - 6
A constraint tells you the least that is known about
T, not its exact type. A common surprise:function f<T extends HasId>(): T { return { id: 0 }; }is an error even though{ id: 0 }fitsHasId. The caller choseT; it might be{ id: number; label: string }, and the function can't produce alabel. TypeScript says'T' could be instantiated with a different subtype of constraint 'HasId'. If the function builds a value, return a type that the function itself controls. - 7
If no constraint is written,
Thas the implicit constraintunknown, which is why nothing is allowed on it. If you want the opposite, "must not benullorundefined", writeT extends {}or use the helperNonNullable<T>(Topic 10.2).
Explain it without notes
What does T extends { length: number } do, and how is it different from taking a parameter of type { length: number }?
Explain function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] piece by piece.
Why is const x: T = { id: 0 } an error when T extends HasId?
What is the constraint of an unconstrained type parameter, and what follows from it?
Practice
Write pluck<T, K extends keyof T>(items: T[], key: K): T[K][] that returns the values of one property from every item. Show that a bad key is rejected.
Write maxBy<T>(items: T[], score: (item: T) => number): T | undefined that returns the item with the highest score. Does it need a constraint? Why or why not?
Write a function hasId<T extends { id: number }>(items: T[], id: number): T | undefined that finds an item by id and returns it with its full type.
Trade-offs
- ↔
Constraints make a function safer to write and read: the signature says exactly what it needs. Over-tight constraints reduce reuse (asking for a whole
Userwhen onlyidis used). Constrain to the smallest shape the body uses. - ↔
A constraint makes error messages mention your helper types (
'HasId'). Name constraint interfaces clearly, so the message teaches the caller what to pass.
Done when you can
I can write
T extends { length: number }and explain what it allows and rejects.I can explain why the result is
Tand not the constraint type.I can write a function with
K extends keyof Tand say whatT[K]returns.I can explain why a function can't create a new
Tfrom a literal.I can say what the constraint of a plain
<T>is.