Generics: Constraints & Defaults
Bound a type parameter so you can safely use its members, and give it a sensible default.
Generics become genuinely powerful once you constrain them. A bare <T> could be anything, so you can barely touch it. Adding extends teaches the compiler what T is guaranteed to have.
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("hi", "hello"); // strings have length
longest([1], [1, 2]); // arrays tooKey-based constraints
The K extends keyof T pattern is the backbone of type-safe property access β the key must be a real key of the object.
Defaults
A type parameter can default, just like a function argument: <T = string>. Callers who don't specify get the default; those who do, override it.
Example
When to use it
- A sort utility constrains its type parameter to Comparable so only types that support ordering can be sorted.
- A repository class defaults its entity type parameter to BaseEntity so simple usages require no explicit type argument.
- A merge utility constrains both type parameters to object to prevent accidental use with primitives.
More examples
Generic with extends constraint
Constrains T to any type with a length property so the function can compare strings, arrays, or custom objects.
interface HasLength { length: number; }
function longest<T extends HasLength>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
console.log(longest('hello', 'hi')); // 'hello'
console.log(longest([1, 2], [3])); // [1, 2]
// longest(1, 2); // Error: number has no .lengthDefault type parameter
A default type parameter (= unknown) makes the type argument optional while still allowing an explicit override.
interface ApiResponse<T = unknown> {
data: T;
status: number;
}
// No type argument needed β defaults to unknown
const raw: ApiResponse = { data: null, status: 200 };
// Explicit type argument
const typed: ApiResponse<{ name: string }> = {
data: { name: 'Alice' },
status: 200,
};Constraint referencing another parameter
K extends keyof T constrains the key parameter to the object's actual keys and infers the exact value type.
function getField<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'Alice', active: true };
const name = getField(user, 'name'); // string
const id = getField(user, 'id'); // number
Discussion