Generics

Generics let you write a function, class or type once and use it with many different types — without giving up type safety. A generic is like a function parameter, except it takes a type instead of a value.

The problem: reusable code loses its types

Suppose you want a helper that returns the first element of any array. Typed with any, it works for everything but tells callers nothing:

TS any-version.ts
function first(items: any[]): any {
  return items[0];
}

const n = first([10, 20]);
console.log(n.toUpperCase);
Output
undefined

n is any, so a nonsense call like n.toUpperCase compiles without complaint. We've lost the fact that an array of numbers gives back a number.

A generic function

TS generic.ts
function first<T>(items: T[]): T | undefined {
  return items[0];
}

const n = first([10, 20]);
const s = first(["a", "b"]);

console.log(n?.toFixed(1), s?.toUpperCase());
Output
10.0 A

<T> declares a type parameter named T. When you call first([10, 20]), TypeScript infers that T is number and the return type becomes number | undefined. Call it with strings and T becomes string. One function, precise types for every caller.

You can also pass the type argument explicitly, which is useful when there's nothing to infer it from:

TS explicit.ts
function emptyList<T>(): T[] {
  return [];
}

const ids = emptyList<number>();
ids.push(7);
console.log(ids);
Output
[ 7 ]

Multiple type parameters

TS pair.ts
function pair<K, V>(key: K, value: V): [K, V] {
  return [key, value];
}

function mapValues<T, U>(items: T[], fn: (item: T) => U): U[] {
  const out: U[] = [];
  for (const item of items) out.push(fn(item));
  return out;
}

const p = pair("age", 31);
const lengths = mapValues(["red", "green"], s => s.length);
console.log(p, lengths);
Output
[ 'age', 31 ] [ 3, 5 ]

In mapValues, T is inferred from the array and U from what the callback returns. This is exactly how the built-in Array.prototype.map is typed.

Single capital letters are the convention for simple cases (T for "type", K/V for key/value, U for a second type). For complex generics, descriptive names like TItem or Row read better.

Generic types and interfaces

Type aliases and interfaces can take type parameters too:

TS generic-types.ts
interface ApiResponse<T> {
  status: number;
  data: T;
}

type Maybe<T> = T | null;

const userRes: ApiResponse<{ name: string }> = { status: 200, data: { name: "Ana" } };
const countRes: ApiResponse<number> = { status: 200, data: 42 };
const missing: Maybe<string> = null;

console.log(userRes.data.name, countRes.data + 1, missing);
Output
Ana 43 null

You've already used built-in generic types: Array<T>, Promise<T>, Map<K, V>, Set<T> and Record<K, V>.

Generic classes

TS stack.ts
class Stack<T> {
  private items: T[] = [];

  push(item: T) {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  get size() {
    return this.items.length;
  }
}

const history = new Stack<string>();
history.push("/home");
history.push("/settings");
console.log(history.pop(), history.size);
Output
/settings 1

Each Stack instance is locked to one element type. history.push(42) would be rejected, because this stack holds strings.

Generics vs any vs unknown

All three let code handle "any type", but they mean different things:

  • any — no checking at all, in or out.
  • unknown — anything can go in, but the caller gets back something they must check.
  • A generic T — anything can go in, and the caller gets back the same type they put in. The relationship between input and output is preserved.

Rule of thumb: if a type appears only once in a signature, you probably don't need a generic. Generics earn their keep when they connect two places — a parameter and the return type, or two parameters.

Try it yourself

Try it yourself
Console output
Next: right now T can be anything, which means you can't do much with it inside the function. The next lesson shows how to constrain a type parameter so you can rely on certain properties existing.