Notifications

No notifications

Generics let you write code that works with *any* type while still keeping that type relationship intact. Instead of any (which discards type info) or one-off copies, a generic captures a type at the call site and threads it through your function, class, or interface. They're how Array, Promise, Map, and every serious React component are typed.

On this page

Detailed Theory

The motivation

// Bad — loses type info
function firstAny(arr: any[]): any { return arr[0]; }

const x = firstAny([1, 2, 3]); // x: any — no autocomplete, no safety

A generic captures the element type and returns it back:

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

const n = first([1, 2, 3]); // number | undefined const s = first(["a", "b"]); // string | undefined const u = first<User>(users); // User | undefined (explicit)

T is a *type parameter* — a placeholder that TS fills in for each call.

Naming

Conventional single-letter names (carry-overs from Java/C++): T (generic), K (key), V (value), E (element), R (return). For complex code, use full words: TItem, TError. Anything is allowed — pick clarity.

Multiple type parameters

function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

const p = pair("age", 21); // [string, number]

Generic interfaces & type aliases

interface Box<T> {
  value: T;
}

const numBox: Box<number> = { value: 1 };

type ApiResponse<T> = { ok: true; data: T } | { ok: false; error: string };

const r: ApiResponse<User> = { ok: true, data: { id: 1, name: "Ada" } as User };

Generic classes

class Stack<T> {
  private items: T[] = [];
  push(x: T) { this.items.push(x); }
  pop(): T | undefined { return this.items.pop(); }
  peek(): T | undefined { return this.items[this.items.length - 1]; }
  get size() { return this.items.length; }
}

const s = new Stack<number>(); s.push(1); s.push(2); const top = s.pop(); // number | undefined

Constraints — extends

Sometimes T shouldn't be *anything*. Use extends to require a shape:

function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest("hello", "hi"); // string longest([1, 2, 3], [1]); // number[] // longest(10, 20); // ❌ number has no .length

keyof + generic — type-safe property access

function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Ada", age: 36 }; const n = get(user, "name"); // string const a = get(user, "age"); // number // get(user, "email"); // ❌ "email" not a key of user

That's the pattern Lodash's _.pick, Zustand selectors, and form libraries lean on.

Default type parameters

interface ApiResponse<T = unknown> {
  ok: boolean;
  data: T;
}

const r: ApiResponse = { ok: true, data: { anything: 1 } };

Useful when callers usually don't care, but power users can override.

Generic functions as values

type Mapper<T, U> = (x: T) => U;

const toLen: Mapper<string, number> = (s) => s.length;

Conditional types — preview

You'll go deeper later, but the pattern starts here:

type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">;   // true
type B = IsString<42>;     // false

Generic constraints with multiple parameters

function merge<A extends object, B extends object>(a: A, b: B): A & B {
  return { ...a, ...b };
}

const m = merge({ id: 1 }, { name: "Ada" }); // { id: number; name: string }

When NOT to use generics

  • One-off helpers with one fixed type — generics add noise.
  • "Just in case" parameters — every T should appear in at least two positions (input AND output, or two inputs that must match).
  • When a union or overload is clearer.

Real-world example — typed local storage

function load<T>(key: string, fallback: T): T {
  try {
    const raw = localStorage.getItem(key);
    return raw ? (JSON.parse(raw) as T) : fallback;
  } catch {
    return fallback;
  }
}

const settings = load("settings", { theme: "light" }); // { theme: string } const score = load<number>("score", 0); // number

> Note the as TJSON.parse returns any. In production you'd validate with Zod and the cast goes away.