Generic Constraints
An unconstrained type parameter T could be anything, so TypeScript will not let you access any properties or methods on a value of type T — it cannot prove they exist. A constraint, written with extends, narrows the set of allowed types so the compiler knows T always has certain members. This makes generics both safe and useful: you keep flexibility across many types while still being able to call .length, read a .id, or index into the value. This page covers basic constraints, constraining to object shapes, the keyof constraint pattern, and constraining one parameter by another.
Why constraints are needed
Without a constraint, the compiler treats T as opaque. Any property access is an error because not every possible T would have that property.
// ❌ Without a constraint
function longest<T>(a: T, b: T): T {
return a.length >= b.length ? a : b;
// ~~~~~~ Error: Property 'length' does not exist on type 'T'.
}
The fix is to tell TypeScript that T must have a length property. We do that by constraining T to a shape that includes it.
Constraining with extends
extends after the type parameter means “T must be assignable to this type.” Here T is bounded to any object with a numeric length, so the body can safely read .length, and the return type stays as specific as the inputs.
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
const arr = longest([1, 2], [1, 2, 3]); // number[]
const str = longest("ab", "abcd"); // string
// longest(10, 20); // ❌ Error: number has no 'length' property
Note the return type is T, not { length: number } — the constraint restricts the input but the function still returns the caller’s exact type. Passing a number is rejected because numbers have no length.
Constraining to object shapes
A common constraint requires elements to have a particular field, such as an id. This lets generic utilities operate over many entity types while relying on shared structure.
interface HasId {
id: number;
}
function indexById<T extends HasId>(items: T[]): Map<number, T> {
const map = new Map<number, T>();
for (const item of items) {
map.set(item.id, item); // safe: every T has `id`
}
return map;
}
const byId = indexById([
{ id: 1, name: "Ada" },
{ id: 2, name: "Linus" },
]); // Map<number, { id: number; name: string }>
The returned values keep their full type ({ id: number; name: string }), not just the HasId part that was required.
The keyof constraint
A frequent pattern constrains one parameter to be a key of another type. This guarantees that a property name actually exists on the object, and lets the return type be the precise type of that property.
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Ada", active: true };
const name = getProp(user, "name"); // string
const active = getProp(user, "active"); // boolean
// getProp(user, "email"); // ❌ Error: '"email"' is not assignable to 'keyof typeof user'
Here K extends keyof T restricts key to one of "id" | "name" | "active", and the return type T[K] (an indexed access type) resolves to the exact property type. Typos and missing keys become compile errors.
Constraining one parameter by another
Type parameters can reference each other in their constraints, which is how you express relationships between several arguments. A common case is requiring a partial object whose keys are a subset of a base type’s keys.
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
const users = [
{ id: 1, name: "Ada" },
{ id: 2, name: "Linus" },
];
const names = pluck(users, "name"); // string[]
const ids = pluck(users, "id"); // number[]
Constraints describe the minimum a type must satisfy, not its exact shape. A constrained
Tcan always have more members than the constraint lists, and your code still sees them.
Constraints vs other tools
| Goal | Tool |
|---|---|
Require T to have certain members | T extends Shape |
| Restrict a key to real properties | K extends keyof T |
| Return the type of a property | T[K] indexed access |
| Allow any type, accept opaqueness | unconstrained T |
| Pick the narrowest literal type | const T parameter |
Best Practices
- Add a constraint as soon as your function body needs to touch a property of
T. - Constrain to the smallest shape that your code actually requires, not a concrete type.
- Use
K extends keyof Tfor any helper that reads or writes a property by name. - Return
T(orT[K]), not the constraint type, so callers keep their precise types. - Remember a constrained
Tmay have extra members; never assume it equals the constraint. - Reference one type parameter from another to model relationships between arguments.
- Prefer constraints over runtime checks when the requirement can be expressed in the type system.