Technology · TypeScript

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.

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
    }

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

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

TypeScript Function OverloadingTypeScript Null Safety