Generic Constraints
Constrain generic type parameters to enforce structural and behavioral contracts at compile time.
You are an expert in Generic Constraints for writing type-safe TypeScript.
## Key Points
- Prefer the narrowest constraint that satisfies the function body; avoid `extends object` when a specific shape is known.
- Use `keyof` constraints instead of accepting raw `string` keys to preserve type-level key information.
- Combine constraints with default type parameters to keep call sites concise while retaining safety.
- Forgetting that `extends` checks structural compatibility, not nominal identity — two unrelated interfaces with the same shape both satisfy the same constraint.
- Over-constraining a generic so that legitimate use cases are rejected; start minimal and widen only when tests reveal a need.
## Quick Example
```typescript
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
```
```typescript
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
```skilldb get typescript-patterns-skills/Generic ConstraintsFull skill: 117 linesGeneric Constraints — TypeScript Patterns
You are an expert in Generic Constraints for writing type-safe TypeScript.
Overview
Generic constraints use the extends keyword to restrict what types a generic parameter can accept. Use them whenever a generic function or class needs to access specific properties or methods on its type parameter, or when you want to limit the set of allowable types to a meaningful subset.
Core Philosophy
Generic constraints embody the principle of "as general as possible, as specific as necessary." An unconstrained generic accepts anything, which means the function body knows nothing about the type and cannot safely access any properties. A constraint narrows the universe of acceptable types to exactly those that have the structure the function needs, giving the body safe access to those properties while remaining open to any type that satisfies the contract.
This is TypeScript's version of bounded polymorphism: you write code once, it works with many types, but only types that meet a clearly stated minimum requirement. The constraint serves as documentation — reading T extends HasId & HasTimestamp tells you immediately what the function expects without examining the implementation. It is a contract between the function author and its callers, enforced by the compiler.
The art of generic constraints lies in finding the right level of specificity. Too broad (no constraint or extends object), and the function body cannot do useful work without casts. Too narrow (constraining to a concrete class), and the function loses its generic value. The sweet spot is constraining to an interface that captures the structural requirement — the properties and methods actually used — and nothing more.
Core Concepts
Basic property constraint:
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
Keyof constraint — limiting keys to those that exist on an object:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
Constraining to a union of types:
function formatId<T extends string | number>(id: T): string {
return `id-${id}`;
}
Multiple constraints via intersection:
interface HasId { id: string }
interface HasTimestamp { createdAt: Date }
function logEntity<T extends HasId & HasTimestamp>(entity: T): void {
console.log(`${entity.id} created at ${entity.createdAt}`);
}
Implementation Patterns
Factory function with constructor constraint:
function create<T>(Ctor: new (...args: any[]) => T, ...args: any[]): T {
return new Ctor(...args);
}
Recursive constraint for tree-like structures:
interface TreeNode<T extends TreeNode<T>> {
children: T[];
parent?: T;
}
interface FileNode extends TreeNode<FileNode> {
name: string;
size: number;
}
Bounded generic with default:
function merge<T extends Record<string, unknown> = Record<string, unknown>>(
target: T,
source: Partial<T>
): T {
return { ...target, ...source };
}
Best Practices
- Prefer the narrowest constraint that satisfies the function body; avoid
extends objectwhen a specific shape is known. - Use
keyofconstraints instead of accepting rawstringkeys to preserve type-level key information. - Combine constraints with default type parameters to keep call sites concise while retaining safety.
Common Pitfalls
- Forgetting that
extendschecks structural compatibility, not nominal identity — two unrelated interfaces with the same shape both satisfy the same constraint. - Over-constraining a generic so that legitimate use cases are rejected; start minimal and widen only when tests reveal a need.
Anti-Patterns
Using any instead of a constraint. Writing function process<T>(item: T) and then casting item as any inside the body to access properties defeats the purpose of generics. Add a constraint that declares the required shape so the compiler can verify both the implementation and every call site.
Constraining to a concrete class instead of an interface. Writing T extends UserEntity when you only need T extends { id: string } couples the generic to a specific implementation. Constrain to the minimal interface so that mocks, DTOs, and alternative implementations all work.
Duplicating constraints across related functions. If several functions share the same constraint, extract it into a named interface. This avoids drift where one function's constraint gets updated but another's does not, and it makes the shared contract explicit.
Forgetting that structural compatibility goes both ways. A constraint T extends { name: string } accepts any object with a name property, including objects with many additional fields. If the function accidentally relies on the absence of extra fields (e.g., for serialization), the constraint is insufficient — add a more precise shape or use a branded type.
Over-constraining with unnecessary intersections. Stacking constraints like T extends A & B & C & D when the function only uses properties from A and B rejects valid inputs needlessly. Only include the interfaces whose members the function actually accesses.
Install this skill directly: skilldb add typescript-patterns-skills
Related Skills
Branded Types
Create nominally distinct types from structural primitives using phantom brands.
Builder Pattern
Implement fluent, type-accumulating builders that enforce required fields at compile time.
Conditional Types
Branch at the type level with conditional expressions, infer, and distributive behavior.
Discriminated Unions
Model mutually exclusive states with tagged union types and exhaustive narrowing.
Module Augmentation
Extend third-party and global type declarations without modifying source using module augmentation.
Template Literal Types
Construct and parse string-level types using template literal syntax for compile-time string validation.