Technology · TypeScript

TypeScript Generics

Master generics to build reusable, type-safe functions, classes, and components in TypeScript.

TL;DR
  1. 01Use type parameters to write reusable, type-safe code.
  2. 02Constrain generics to restrict what types they accept.
  3. 03Provide default types to simplify common generic usage.

Basics

  • Add a type parameter in angle brackets to make a function generic.
    function identity<T>(value: T): T {
      return value;
    }
  • The letter T is a placeholder that gets replaced with a real type.
  • TypeScript infers the type from the argument you pass to the function.
    identity("hello"); // T inferred as string
    identity(42);      // T inferred as number
  • You can also pass the type explicitly using angle brackets at the call.
    identity<string>("hello");
  • Use single uppercase letters or short descriptive names for type parameters.

Generic Functions

  • Write a generic function once and reuse it for many different types.
    function first<T>(items: T[]): T | undefined {
      return items[0];
    }
  • Return values keep their original type, unlike using any for parameters.
  • Pass arrays generically with T[] or Array<T> for typed list operations.
  • Use multiple type parameters when handling pairs of types.
    function pair<T, U>(a: T, b: U): [T, U] {
      return [a, b];
    }
  • Chain generics through helper functions to keep type information intact.

Constraints

  • Limit a type parameter using extends to require certain properties.
    function byId<T extends { id: number }>(items: T[], id: number) {
      return items.find(i => i.id === id);
    }
  • Constraints make sure the type has the properties your function needs.
  • Use keyof T to constrain a parameter to keys of another type.
    function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
      return obj[key];
    }
  • Combine constraints with conditional types for advanced type narrowing.
  • Constraints still allow types that include extra properties beyond the requirement.

Default Types

  • Set a default type to simplify common use cases for callers.
    interface ApiResponse<T = unknown> {
      data: T;
      status: number;
    }
  • Default types let callers skip the type argument when the default fits.
    const res: ApiResponse = { data: null, status: 200 };
  • Combine defaults with constraints to keep types both safe and easy.
    type Config<T extends object = {}> = { values: T };
  • Use defaults to keep public APIs clean while supporting custom overrides.
  • Defaults are especially useful for generic React components and utility types.

Generic Classes

  • Add type parameters to class names for typed instances and methods.
    class Box<T> {
      constructor(public value: T) {}
      get() { return this.value; }
    }
  • Use the type parameter inside properties, methods, and constructor signatures.
  • Pass the type when creating an instance, or let it be inferred.
    const a = new Box<string>("hello");
    const b = new Box(42); // inferred as Box<number>
  • Generic classes work well for collections, queues, stacks, and state stores.
  • TypeScript will often infer the type from the constructor argument automatically.

Tip: Name generics meaningfully, like TItem or TResponse, when single letters make the code harder to read.

Warning: Avoid overusing generics in simple functions, since they can make code harder to read and maintain.

TypeScript ClassesTypeScript Interfaces