Generics are what make typed collections, promises, and reusable helpers possible; every library API uses them.
Suppose you write function first(items: any[]): any. It works for any array, but the result is any — you've lost the information that a string[] yields a string. Generics fix this with a type parameter, a variable that stands for a type:
function first<T>(items: T[]): T | undefined {
return items[0];
}
const n = first([1, 2, 3]); // T inferred as number → n: number | undefinedYou declare type parameters in angle brackets and use them in the signature. At each call the compiler infers T from the arguments; you can also pass it explicitly — first<string>([]) — which is handy when there's nothing to infer from.
Types can be generic too. You already use them: Array<T>, Promise<T>, Map<K, V>, Set<T>. Define your own with aliases, interfaces and classes:
interface ApiResponse<T> { data: T; status: number }
type Pair<A, B = A> = [A, B]; // B defaults to AGuidelines for good generic signatures:
- A type parameter should relate two or more things — an input to an output, or two inputs. If
Tappears only once, you probably don't need it; useunknownor a concrete type. - Use as few type parameters as possible and let inference do the work; callers should rarely need to write
<...>. - Name them
T,K,V,Efor simple cases, or descriptive names (TItem) in complex signatures.
Generic arrow functions are written const id = <T,>(x: T) => x; — the trailing comma avoids JSX ambiguity in .tsx files.
Example
function first<T>(items: readonly T[]): T | undefined {
return items[0];
}
function groupBy<T, K extends PropertyKey>(items: T[], key: (item: T) => K): Record<K, T[]> {
const out = {} as Record<K, T[]>;
for (const item of items) {
(out[key(item)] ??= []).push(item);
}
return out;
}
interface ApiResponse<T> { data: T; status: number }
type Pair<A, B = A> = [A, B];
const word = first(["a", "b"]); // string | undefined
const byLength = groupBy(["hi", "yo", "hey"], s => s.length); // Record<number, string[]>
const res: ApiResponse<{ id: number }[]> = { data: [{ id: 1 }], status: 200 };
const range: Pair<number> = [0, 10];
class Stack<T> {
#items: T[] = [];
push(item: T): void { this.#items.push(item); }
pop(): T | undefined { return this.#items.pop(); }
}
const s = new Stack<string>();
s.push("x");
console.log(word, byLength[3], res.data[0]?.id, range, s.pop());Remember
- A type parameter
<T>links input types to output types - Type arguments are usually inferred; pass them explicitly only when needed
- Interfaces, aliases and classes can be generic too
- If a type parameter is used only once, you likely don't need it
Watch out for
- Writing
function f<T>(x: T): voidwhereTrelates nothing —unknownsays the same thing more simply.