Skip to main content
Technology & EngineeringTypescript Patterns117 lines

Generic Constraints

Constrain generic type parameters to enforce structural and behavioral contracts at compile time.

Quick Summary25 lines
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 lines
Paste into your CLAUDE.md or agent config

Generic 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 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.

Common Pitfalls

  • 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.

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

Get CLI access →