Introduction
Sometimes a generic function needs to access specific properties on its type parameter. Without constraints, T could be anything, so TypeScript cannot guarantee any properties exist. Generic constraints use the extends keyword to limit what types are acceptable, giving you access to their known properties.
Key Concepts
- Generic Constraint (
extends): Limits a type parameter to types that match a specific shape. - Minimum Shape: The constraint defines the minimum set of properties a type must have.
- Default Type Parameters: A fallback type used when the caller does not specify a type argument.
Real World Context
A generic sortBy function needs to know that items have a comparable property. A merge function needs to know its arguments are objects. Constraints encode these requirements so the compiler enforces them and the function body can safely access the required properties.
Deep Dive
The Problem Without Constraints
Without a constraint, accessing properties on T is an error:
typescriptfunction getLength<T>(value: T): number { return value.length; // Error: Property 'length' does not exist on type 'T' }
TypeScript does not know that T has a length property. It could be number, which has no length.
Adding a Constraint
Use extends to require a minimum shape:
typescriptfunction getLength<T extends { length: number }>(value: T): number { return value.length; // OK — T is guaranteed to have .length } getLength("hello"); // OK — string has .length getLength([1, 2, 3]); // OK — array has .length // getLength(42); // Error — number has no .length
The constraint { length: number } means: any type is accepted as long as it has a numeric length property.
Constraining with Interfaces
For complex constraints, use named interfaces:
typescriptinterface HasId { id: string; } function findById<T extends HasId>(items: T[], targetId: string): T | undefined { return items.find(item => item.id === targetId); } const users = [ { id: "u1", name: "Alice", role: "admin" }, { id: "u2", name: "Bob", role: "user" }, ]; const found = findById(users, "u1"); // found is { id: string; name: string; role: string } | undefined
The constraint ensures every item has an id, but the full type (with name and role) is preserved in the return type.
The keyof Constraint
A common pattern is constraining one parameter based on another:
typescriptfunction getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const user = { name: "Alice", age: 30, email: "alice@example.com" }; const name = getProperty(user, "name"); // string const age = getProperty(user, "age"); // number // getProperty(user, "phone"); // Error: 'phone' is not a key of user
The constraint K extends keyof T ensures the key is a valid property of the object, and the return type T[K] is the exact type of that property.
Default Type Parameters
Type parameters can have defaults:
typescriptinterface PaginatedList<T, TMetadata = { total: number; page: number }> { items: T[]; metadata: TMetadata; } // Using the default metadata type const userList: PaginatedList<User> = { items: [{ id: "1", name: "Alice" }], metadata: { total: 100, page: 1 }, }; // Overriding the metadata type const detailedList: PaginatedList<User, { total: number; page: number; hasMore: boolean }> = { items: [], metadata: { total: 0, page: 1, hasMore: false }, };
Default type parameters work like default function parameters — they provide a fallback when the type is not explicitly specified.
Common Pitfalls
- Over-constraining — Do not add constraints the function body does not need. If the function does not access
.length, do not require it. - Confusing
extendsin generics with class inheritance — In a generic constraint,T extends Shapemeans "T must be assignable to Shape," not "T must be a subclass of Shape."
Best Practices
- Use
keyofconstraints for property access — ThegetPropertypattern ensures type-safe dynamic property access. - Provide default type parameters for common cases — This simplifies usage for the most common scenario while allowing customization.
Summary
extendsconstrains a generic to types matching a minimum shape.- Named interfaces make complex constraints readable.
keyofconstraints enable type-safe property access patterns.- Default type parameters simplify common usage while allowing overrides.
Code Examples
// keyof constraint for type-safe property access
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
const products = [
{ name: "Keyboard", price: 79, inStock: true },
{ name: "Mouse", price: 29, inStock: false },
];
const names = pluck(products, "name"); // string[]
const prices = pluck(products, "price"); // number[]
// pluck(products, "color"); // Error: 'color' is not a key of the product type