Constraints let generic code use properties of T safely; without them generics can do almost nothing.
An unconstrained T could be anything, so inside the function you can't touch any of its properties. A constraint says what T must at least be:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("abc", "de"); // T = string
longest([1, 2], [3]); // T = number[]
longest(10, 20); // error: number has no lengthNote that the function returns T, not { length: number } — callers keep their precise type. That's the advantage over simply typing the parameter as { length: number }.
Constraints can refer to other type parameters. The classic case is a typed property getter:
function prop<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; }keyof T is the union of T's property names, so K must be a real key, and T[K] is the type of that property (both operators are covered in depth later). prop(user, "nmae") is a compile error and prop(user, "age") returns number.
Defaults work like default parameters: interface Box<T = string> lets callers write Box alone. A default must satisfy the constraint if there is one: <T extends object = {}>.
A subtle point: a constraint is a lower bound on knowledge, not the type itself. Inside function f<T extends string>(x: T) you can't return "hello" as T, because T might be the narrower literal "bye". When you find yourself needing as T, the signature usually needs rethinking.
When inference picks a type from the "wrong" argument, NoInfer<T> (TS 5.4) marks a position that should not be used for inference.
Example
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
function prop<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
interface Paginated<T = string> { items: T[]; next?: string }
function pickColor<C extends string>(colors: C[], fallback: NoInfer<C>): C {
return colors[0] ?? fallback;
}
const user = { name: "Ada", age: 36 };
const longer = longest("abc", "de"); // string
const age = prop(user, "age"); // number
const page: Paginated = { items: ["a"] }; // T defaults to string
const c = pickColor(["red", "blue"], "red"); // "red" | "blue"
// @ts-expect-error -- "nmae" is not assignable to "name" | "age"
prop(user, "nmae");
// @ts-expect-error -- "green" not in "red" | "blue" thanks to NoInfer
pickColor(["red", "blue"], "green");
console.log(longer, age, page.items, c);Remember
T extends Xlets you use X's members on T while keeping T preciseK extends keyof T+T[K]type property access safely- Defaults (
<T = string>) make generics easier to use NoInfer<T>stops a parameter from influencing inference
Watch out for
- Returning a concrete value
as T— the caller's T may be narrower than the constraint.