TypeScript Generics Constraints

Build flexible but safe types using constrained generics.

TL;DR

  1. 01Use extends to constrain generic types to specific shapes.
  2. 02Use keyof to safely access object properties.
  3. 03Combine constraints with conditional types for powerful patterns.

Tips

  1. 01Use constraints to catch errors at compile time instead of runtime — this prevents many subtle bugs in generic code.

Warnings

  1. 01Over-constraining generics can make types too restrictive — find the right balance between flexibility and safety.

Basic Constraints

  • Constrain generic type to a specific type with extends.
    function getLength<T extends { length: number }>(item: T): number {
      return item.length;
    }
    
    getLength("hello");     // works: string has length
    getLength([1, 2, 3]);   // works: array has length
    getLength(123);         // error: number has no length
  • Constrain to primitive types.
    function identity<T extends string | number>(value: T): T {
      return value;
    }
    
    identity("text");  // works
    identity(42);      // works
    identity({});      // error
  • Extend specific classes or interfaces.
    interface HasId {
      id: string;
    }
    
    function getId<T extends HasId>(item: T): string {
      return item.id;
    }

Keyof Constraints

  • Use keyof to safely access object properties.
    function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
      return obj[key];
    }
    
    const user = { name: "Alice", age: 30 };
    getProperty(user, "name"); // works: "name" is a key
    getProperty(user, "email"); // error: "email" is not a key
  • Create flexible getters with key constraints.
    function getProp<T extends object, K extends keyof T>(
      obj: T,
      key: K
    ): T[K] {
      return obj[key];
    }

Multiple Constraints

  • Combine constraints with intersection types.
    interface HasId {
      id: string;
    }
    
    interface HasName {
      name: string;
    }
    
    function process<T extends HasId & HasName>(item: T): string {
      return `${item.id}: ${item.name}`;
    }
  • Constrain multiple generic parameters.
    function merge<T extends object, U extends object>(
      obj1: T,
      obj2: U
    ): T & U {
      return { ...obj1, ...obj2 };
    }
  • Use conditional constraints for complex logic.
    function getValue<T extends string | number | boolean>(
      value: T
    ): T extends string ? string : T extends number ? number : boolean {
      return value;
    }

Constructor Constraints

  • Constrain generic to a class constructor.
    function create<T extends { new (...args: any[]): {} }>(
      constructor: T
    ): InstanceType<T> {
      return new constructor();
    }
    
    class User { }
    const user = create(User); // user is typed as User
  • Accept class instances with type preservation.
    function instantiate<T extends new () => any>(
      Class: T
    ): InstanceType<T> {
      return new Class();
    }

Practical Patterns

  • Create type-safe array utilities with constraints.
    function findById<T extends { id: string | number }>(
      items: T[],
      id: string | number
    ): T | undefined {
      return items.find(item => item.id === id);
    }
  • Build flexible mappers with key constraints.
    function mapProperty<T, K extends keyof T>(
      items: T[],
      key: K
    ): T[K][] {
      return items.map(item => item[key]);
    }
    
    const names = mapProperty(users, "name");
  • Create type-safe event handlers.
    type EventMap = {
      login: { userId: string };
      logout: { timestamp: number };
    };
    
    function on<E extends keyof EventMap>(
      event: E,
      handler: (data: EventMap[E]) => void
    ) {
      // Type-safe handler
    }

FAQ