TypeScript Generics Explained
Writing functions and types that work with a placeholder type instead of one fixed type.
2 min read
Generics let a function, interface, or type work with a placeholder type instead of one specific type — filled in with a real type each time it's actually used. They're what lets TypeScript's built-in Array<T>, Promise<T>, and Map<K, V> work generically across any type you put in them, and you can write the same kind of reusable code yourself.
The problem generics solve
function firstElement(arr: number[]): number {
return arr[0];
}This only works for arrays of numbers. Writing a separate, nearly identical function for every other array type doesn't scale — and using any throws away all type safety:
function firstElement(arr: any[]): any {
return arr[0]; // no type information preserved at all
}
const result = firstElement(["a", "b", "c"]); // result is typed `any`, not `string`Generics: a type parameter instead
function firstElement<T>(arr: T[]): T {
return arr[0];
}
firstElement([1, 2, 3]); // inferred as number
firstElement(["a", "b", "c"]); // inferred as string<T> declares a type parameter — a placeholder filled in based on what's actually passed. Call it with a number[], and every T in the function's signature becomes number for that call; call it with a string[], and T becomes string. Unlike the any version, the return type is correctly preserved — TypeScript knows firstElement(["a", "b"]) returns a string, not just "anything."
T is a convention, not a keyword — you could name it anything, but T (and K, V for a key/value pair) is the near-universal naming convention, borrowed from similar generic systems in other typed languages.
Generic interfaces
interface Box<T> {
value: T;
}
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: "hello" };This is exactly the pattern behind Array<T> and Promise<T> — a reusable shape parameterized by whatever type it's holding.
Multiple type parameters
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
pair("age", 36); // [string, number] -- both inferred independentlyConstraining a generic type
extends on a type parameter restricts what's allowed, so you can safely rely on properties that any valid input is guaranteed to have:
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("hello"); // fine -- strings have length
getLength([1, 2, 3]); // fine -- arrays have length
getLength(42); // Argument of type 'number' is not assignableWithout the constraint, TypeScript wouldn't let you access .length at all, since a fully unconstrained T could be anything, including a type with no .length property.
Generics are what makes TypeScript's built-in utility types — Partial<T>, Pick<T, K>, Record<K, V>, covered next — possible in the first place. They're functions that operate on types themselves, using exactly this mechanism.