Difficulty: Intermediate
How do you constrain generic types in TypeScript? Explain extends constraints, default types, and when to use them.
Generic constraints restrict what types can be passed as type arguments. Without constraints, a generic T could be anything. With constraints (T extends SomeType), T must be assignable to SomeType.
Key patterns: - Simple constraint: <T extends object> (T must be an object) - Property constraint: <T extends { length: number }> (T must have length) - Key constraint: <K extends keyof T> (K must be a key of T) - Multiple constraints: <T extends A & B> (T must satisfy both A and B) - Default types: <T = string> (T defaults to string if not specified)
Constraints make generics more useful by enabling property access and method calls on the constrained type.
// Without constraint - cannot access any properties
function badLength<T>(value: T): number {
// return value.length; // Error: Property 'length' does not exist on type 'T'
return 0;
}
// With constraint - can access .length
function getLength<T extends { length: number }>(value: T): number {
return value.length; // OK: T has 'length'
}
getLength('hello'); // OK: string has length
getLength([1, 2, 3]); // OK: array has length
getLength({ length: 10 }); // OK: has length property
// getLength(42); // Error: number has no 'length'
// keyof constraint for safe property access
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 30, email: 'a@b.com' };
getProperty(user, 'name'); // string
getProperty(user, 'age'); // number
// getProperty(user, 'foo'); // Error: 'foo' not assignable to keyof user
// Constraint with class
function createInstance<T extends new (...args: any[]) => any>(
Ctor: T,
...args: ConstructorParameters<T>
): InstanceType<T> {
return new Ctor(...args);
}
Constraints tell TypeScript what capabilities T has. Without them, you cannot access any properties. The extends keyword limits T to types that have the required shape.
// Multiple constraints via intersection
interface Serializable {
serialize(): string;
}
interface Identifiable {
id: string | number;
}
function save<T extends Serializable & Identifiable>(entity: T): void {
const data = entity.serialize(); // OK: Serializable
console.log(`Saving entity ${entity.id}: ${data}`); // OK: Identifiable
}
// Default type parameters
interface ApiResponse<T = unknown, E = string> {
data: T;
error?: E;
status: number;
}
type UserResponse = ApiResponse<User>; // E defaults to string
type SimpleResponse = ApiResponse; // T = unknown, E = string
type DetailedError = ApiResponse<User, Error>; // both specified
// Generic class with defaults
class Store<K = string, V = unknown> {
private data = new Map<K, V>();
set(key: K, value: V): void { this.data.set(key, value); }
get(key: K): V | undefined { return this.data.get(key); }
}
const stringStore = new Store(); // Store<string, unknown>
const typedStore = new Store<number, User>(); // Store<number, User>
Use intersection (&) to require multiple constraints. Default type parameters provide sensible fallbacks, reducing boilerplate for common use cases.
// Constrained merge function
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
const result = merge({ name: 'Alice' }, { age: 30 });
// { name: string; age: number }
// merge(42, 'hello'); // Error: number is not assignable to object
// Constrained factory
interface Model {
id: string;
createdAt: Date;
}
function createModel<T extends Model>(data: Omit<T, 'id' | 'createdAt'>): T {
return {
...data,
id: crypto.randomUUID(),
createdAt: new Date(),
} as T;
}
// Recursive constraint
interface TreeNode<T extends TreeNode<T>> {
value: string;
children: T[];
}
interface FileNode extends TreeNode<FileNode> {
type: 'file';
size: number;
}
interface FolderNode extends TreeNode<FolderNode | FileNode> {
type: 'folder';
}
// Constrained comparison
function max<T extends number | string | Date>(a: T, b: T): T {
return a > b ? a : b;
}
max(10, 20); // number
max('apple', 'banana'); // string
// max(10, 'hello'); // Error: can't mix types
Constraints ensure generic functions only accept valid types. Recursive constraints model tree structures where nodes reference their own type.
Generic Constraints, extends keyword, Default Type Parameters, Multiple Constraints, Bounded Polymorphism