Generic Constraints
Limit type parameters with extends so generics stay flexible but can safely use properties like id, length, or keyof T.
- typescript
- generic-constraints
Unconstrained generics accept everything — which means you can do almost nothing inside the function body. Constraints (T extends …) declare the minimum shape T must have so the implementation can use real properties while callers stay polymorphic.
Docs: Generics — Constraints, keyof.
The problem without constraints
function getId<T>(item: T) {
// return item.id; // Error: Property 'id' does not exist on type 'T'
}
Constraint with extends
function getId<T extends { id: string }>(item: T): string {
return item.id;
}
getId({ id: '1', name: 'Ada' }); // ok
// getId({ name: 'Ada' }); // error
T is still a specific type at each call site — you get the full object type in, not a stripped { id: string } only, when you return T:
function withLog<T extends { id: string }>(item: T): T {
console.log(item.id);
return item;
}
const u = withLog({ id: '1', role: 'admin' as const });
// u.role is 'admin'
keyof constraints
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: '1', age: 30 };
const age = pluck(user, 'age'); // number
// pluck(user, 'nope'); // error
This is the core of typed form field helpers, translators, and selectors.
Multiple bounds via intersection
type Identified = { id: string };
type Timestamped = { updatedAt: string };
function save<T extends Identified & Timestamped>(entity: T): T {
return { ...entity, updatedAt: new Date().toISOString() };
}
Or constrain to a union of allowed primitives:
function parseId<T extends string | number>(raw: T): T {
return raw;
}
Constraining to constructors / instances
function create<T>(Ctor: new () => T): T {
return new Ctor();
}
class Store {}
const s = create(Store); // Store
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest([1, 2], [1, 2, 3]); // number[]
longest('ab', 'abcd'); // string
Default type parameters + constraints
function createMap<K extends string | number | symbol = string, V = unknown>() {
return new Map<K, V>();
}
Defaults keep call sites clean; constraints keep misuse loud.
React prop pattern
type ListProps<T extends { id: string }> = {
items: T[];
onSelect: (item: T) => void;
};
function List<T extends { id: string }>({ items, onSelect }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
<button type="button" onClick={() => onSelect(item)}>
{item.id}
</button>
</li>
))}
</ul>
);
}
Don’t over-constrain
// Too tight — forces full User when only id needed
function ban<T extends User>(user: T) { /* … */ }
// Better
function ban(user: { id: string }) { /* … */ }
// or
function ban<T extends { id: string }>(user: T): T { /* … */ }
If you never use T in the return type or elsewhere, a concrete parameter type is simpler than a generic.
Constraints vs type guards
Constraints are static. They don’t validate runtime JSON:
function trustMe<T extends { id: string }>(data: T) {
return data.id;
}
// trustMe(JSON.parse(x)) // still a cast/lie if you force T
Pair API boundaries with runtime validation and unknown.
Footguns
T extends any— disables useful checking; avoid.T extends object— excludes primitives but allows arrays; be intentional.- Circular constraints — simplify with intermediate types.
- Using constraints to “document” without using the property — delete the generic.
Interview out-loud answer
“Constraints limit type parameters so the body can use properties safely while preserving the caller’s concrete type. K extends keyof T is the classic key-safe accessor. I avoid generics when a concrete type would do, and I never treat a constraint as runtime validation.”
Related on this site
- Generics basics
- Generics in React components
- keyof typeof and indexed access
- Conditional types intro
- unknown vs any
Further reading
Related guides
- Basic Types and AnnotationsPrimitives, arrays, objects, and function annotations TypeScript actually checks — plus where inference is enough and where it is not.
- Branded Types for IDsNominal-style UserId vs OrderId in TypeScript — prevent ID mixups at compile time with brands, parsers, and form boundaries.
- Conditional Types IntroT extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.
- Declaration Files and DefinitelyTypedHow .d.ts files describe JS to TypeScript, when to use @types packages, module augmentation, and writing minimal ambient types for untyped libs.
- Discriminated Unions for UI StateModel loading, success, and error as mutually exclusive variants so TypeScript and your UI cannot show impossible states.