Design.dev design.dev

TypeScript generics

Generics let you write reusable components and functions that work across many types while preserving relationships (for example, “input and output share the same shape”).

Generic functions

Type parameters appear in angle brackets before the parameter list.

function identity<T>(value: T): T {
  return value;
}

const n = identity(42);       // T inferred as number
const s = identity<string>('x'); // explicit T

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

Multiple type parameters

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

Generic interfaces and type aliases

interface ApiResponse<T> {
  data: T;
  meta: { requestId: string };
}

type Box<T> = { value: T };

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

Constraints with extends

Require that a type parameter has certain properties or extends a base type.

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

function logLength<T extends { length: number }>(x: T) {
  console.log(x.length);
}
logLength('hi');
logLength([1, 2, 3]);

keyof T produces a union of known keys; combined with generics it models safe property access.

Default type parameters

interface FetchOptions<T = unknown> {
  url: string;
  parse?: (body: string) => T;
}

type Nullable<T = string> = T | null;

If inference does not apply, the default is used; callers can still supply a specific type.

Practical patterns

Wrapped API result

type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

async function fetchJson<T>(url: string): Promise<Result<T>> {
  try {
    const res = await fetch(url);
    if (!res.ok) return { ok: false, error: res.statusText };
    const raw: unknown = await res.json();
    // `as T` does not validate — only use after you trust the origin or parse/validate `raw`
    const value = raw as T;
    return { ok: true, value };
  } catch (e) {
    return { ok: false, error: e instanceof Error ? e.message : 'Unknown' };
  }
}

Security: JSON from the network is untrusted. Assertions like as T are erased at compile time and do not protect against malicious or malformed payloads. For production, validate raw with a schema library or explicit checks before treating it as T.

Preserve tuple types

function tuple<T extends readonly unknown[]>(...args: T): T {
  return args;
}
const t = tuple(1, 'a', true); // readonly [number, string, boolean]