TypeScript Generics Constraints

A reference guide to TypeScript generic constraints using extends, keyof, constructor signatures, and intersections.

TL;DR

  1. Constrain type parameters using the extends keyword with contracts.
  2. Limit generic access to valid property keys with keyof indexing.
  3. Combine multiple constraint requirements using the intersection & operator.

Structural Constraints

    extends object shape

    Requires an argument to possess a minimum set of properties.

    interface HasId { id: string; }
    function printId<T extends HasId>(item: T): string {
      return item.id;
    }
    length constraint

    Restricts parameters to strings, arrays, or countable objects.

    function getLength<T extends { length: number }>(
      val: T
    ): number {
      return val.length;
    }
    primitive union limit

    Limits generics strictly to scalar string or number types.

    function identity<T extends string | number>(v: T): T {
      return v;
    }

Keyof Index Constraints

    keyof property lookup

    Safely reads dynamic object fields without type assertions.

    function getProp<T, K extends keyof T>(
      obj: T,
      key: K
    ): T[K] {
      return obj[key];
    }
    property setter helper

    Constrains updated values to the target property type.

    function setProp<T, K extends keyof T>(
      obj: T,
      key: K,
      val: T[K]
    ): void {
      obj[key] = val;
    }
    multiple key selection

    Extracts subsets of properties using array key constraints.

    function pluck<T, K extends keyof T>(
      items: T[],
      key: K
    ): T[K][] {
      return items.map(item => item[key]);
    }

Multiple Intersected Constraints

    intersected contracts

    Requires satisfying multiple contracts simultaneously.

    interface Named { name: string; }
    interface Aged { age: number; }
    function verify<T extends Named & Aged>(
      val: T
    ): string {
      return `${val.name} is ${val.age}`;
    }
    class and interface merge

    Ensures an entity extends a base class and satisfies traits.

    class Model {}
    interface Syncable { sync(): void; }
    function save<T extends Model & Syncable>(m: T) {
      m.sync();
    }
    record constraint

    Enforces dictionary structure while narrowing value types.

    function logMap<T extends Record<string, number>>(
      dict: T
    ): void {
      for (const [k, v] of Object.entries(dict)) {
        console.log(`${k}: ${v}`);
      }
    }

Constructor Constraints

    newable constructor

    Constrains arguments to valid instantiable class types.

    type Constructor<T> = new (...args: any[]) => T;
    function createInstance<T>(ctor: Constructor<T>): T {
      return new ctor();
    }
    factory with arguments

    Passes constructor arguments through generic factory helpers.

    function createWithArgs<T, A extends any[]>(
      ctor: new (...args: A) => T,
      ...args: A
    ): T {
      return new ctor(...args);
    }
    abstract class constructor

    References abstract constructor signatures in mixin helpers.

    type AbstractConstructor<T> = abstract new (
      ...args: any[]
    ) => T;

Conditional Constraints

    nested conditional limits

    Refines types based on generic constraint evaluations.

    type Unpack<T> = T extends (infer U)[] ? U : T;
    type Num = Unpack<number[]>; // number
    filtering never constraint

    Excludes incompatible union members using never.

    type NonEmpty<T> = T extends '' | null | undefined
      ? never
      : T;
    infer in constraint

    Extracts inner component types through pattern matching.

    type ItemType<T> = T extends { items: (infer I)[] }
      ? I
      : never;

Tips

  1. Use K extends keyof T to guarantee that property lookup helpers return precise property values without casting.
  2. Apply constructor signatures like new (...args: any[]) => T when authoring generic factory methods or dependency injection containers.

Warnings

  1. Avoid over-constraining <T> with excessive properties because it limits caller flexibility and clutters function signatures unnecessarily.
  2. Remember that extends constraints enforce minimum structural requirements rather than exact nominal matches, permitting excess properties at runtime.

In Practice

FAQ