GGistDev

Generics in TypeScript

Generics let you write reusable, type-safe code by parameterizing types.

Generic functions

function first<T>(xs: T[]): T | undefined {
  return xs[0];
}

function mapValues<T, U>(obj: Record<string, T>, f: (x: T) => U): Record<string, U> {
  const out: Record<string, U> = {};
  for (const k in obj) out[k] = f(obj[k]);
  return out;
}

Type arguments are usually inferred at call sites.

Constraints with extends

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

K extends keyof T ties the key to the object shape.

Default type parameters

interface Box<T = string> { value: T }
const b: Box = { value: "hi" }; // T defaulted to string

Generic interfaces and classes

interface Result<T, E = Error> {
  ok: boolean;
  value?: T;
  error?: E;
}

class Queue<T> {
  #items: T[] = [];
  enqueue(x: T) { this.#items.push(x); }
  dequeue(): T | undefined { return this.#items.shift(); }
}

Variance and readonly

Use readonly arrays to avoid accidental unsound writes when variance matters.

function readAll(xs: ReadonlyArray<string>) { xs.forEach(console.log); }

Builder-style APIs with generics

type WithAuth<T> = T & { auth: { userId: string } };
function withAuth<T>(x: T): WithAuth<T> { return Object.assign(x, { auth: { userId: "u1" } }); }

Utility patterns

  • Array<T> vs T[]: stylistic
  • Use Record<K, V> for dictionary types
  • Prefer generic constraints over any

Summary

  • Let inference do the work; constrain with extends when needed
  • Defaults reduce verbosity; model APIs with generic interfaces/classes