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:
function first(items: any[]): any { return items[0]; } const n = first([10, 20]); console.log(n.toUpperCase);
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
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());
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:
function emptyList<T>(): T[] { return []; } const ids = emptyList<number>(); ids.push(7); console.log(ids);
[ 7 ]
Multiple type parameters
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);
[ '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:
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);
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
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);
/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
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.