Generics
Suppose you write a function that returns the first item of an array. It should work for an array of names, an array of prices and an array of trains, and the result should keep the right type each time. Writing one version per type is repetitive, and using any throws the types away. Generics solve this: they let a function, type or class work with many types while staying fully typed.
You have already used generics without noticing: Array<string>, Record<string, number> and Map<string, number> are all generic types. In this lesson you will write your own.
Your first generic function
A generic function has a type parameter in angle brackets, usually named T (for "type"). T is a placeholder that is filled in each time the function is called.
function firstItem<T>(items: T[]): T | undefined {
return items[0];
}
const city = firstItem(["Jaipur", "Pune", "Kochi"]); // T is string
const price = firstItem([1299, 499]); // T is number
const nothing = firstItem<boolean>([]); // T given explicitly
console.log(city?.toUpperCase());
console.log(price?.toFixed(2));
console.log(nothing);JAIPUR 1299.00 undefined
When you call firstItem(["Jaipur", ...]), TypeScript infers T as string from the argument, so city has the type string | undefined and toUpperCase() is allowed. You can also pass the type yourself, as in firstItem<boolean>([]), which is useful when there is nothing to infer from.
Compare this with function firstItem(items: any[]): any. That version also runs, but city would be any, and a typo like city.toUppercase() would go unnoticed. Generics keep the connection between the input type and the output type.
Several type parameters
A function can have more than one type parameter. Here K is the key type and V is the value type:
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
function mapValues<V, R>(record: Record<string, V>, fn: (value: V) => R): Record<string, R> {
const result: Record<string, R> = {};
for (const [key, value] of Object.entries(record)) {
result[key] = fn(value);
}
return result;
}
const entry = pair("PNR", 4521890); // [string, number]
console.log(entry);
const fees = { aarav: 12000, meera: 9500, rohan: 15000 };
const withLabel = mapValues(fees, (fee) => `₹${fee.toLocaleString("en-IN")}`);
console.log(withLabel);[ 'PNR', 4521890 ]
{ aarav: '₹12,000', meera: '₹9,500', rohan: '₹15,000' }In mapValues, TypeScript infers V as number from fees and R as string from what the callback returns, so withLabel is a Record<string, string>.
Constraints with extends
Inside a generic function, T could be anything, so TypeScript only lets you do what works for every type. To use a property such as .length, add a constraint: T extends { length: number } means "T can be any type that has a numeric length".
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("Lucknow", "Pune");
longest([1, 2, 3], [4, 5]);
longest(10, 20);app.ts(7,9): error TS2345: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.Strings and arrays have length, so the first two calls work and return a string and a number array. Numbers do not, so the third call is rejected.
A very common constraint is K extends keyof T, which means "K must be one of the property names of T". It lets you write safe helpers that read properties by name:
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
const students = [
{ name: "Isha", marks: 88, city: "Kochi" },
{ name: "Farhan", marks: 74, city: "Lucknow" },
];
const names = pluck(students, "name"); // string[]
const marks = pluck(students, "marks"); // number[]
console.log(names, marks);
console.log(Math.max(...marks));[ 'Isha', 'Farhan' ] [ 88, 74 ] 88
pluck(students, "age") would be a compile error, because "age" is not a key of the student type. The return type T[K][] means "an array of whatever type that property has". You will study keyof and T[K] in detail in keyof, typeof & Indexed Access.
Generic types and classes
Type aliases, interfaces and classes can be generic too. A typical example is the result of an API call, which has the same outer shape whatever data it carries:
type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: string };
class Queue<T> {
#items: T[] = [];
enqueue(item: T): void {
this.#items.push(item);
}
dequeue(): T | undefined {
return this.#items.shift();
}
get size(): number {
return this.#items.length;
}
}
function show<T>(result: ApiResult<T>, format: (data: T) => string): string {
return result.ok ? format(result.data) : `Failed: ${result.error}`;
}
const tokens = new Queue<number>();
tokens.enqueue(101);
tokens.enqueue(102);
console.log(tokens.dequeue(), tokens.size);
const results: ApiResult<number[]>[] = [
{ ok: true, data: [12, 13, 14] },
{ ok: false, error: "Server busy" },
];
for (const result of results) {
console.log(show(result, (seats) => `Seats ${seats.join(", ")}`));
}101 1 Seats 12, 13, 14 Failed: Server busy
Queue<number> only accepts numbers: tokens.enqueue("A1") would be an error. The same Queue class works for patient tokens, print jobs or messages, each with full type checking.
Default type parameters
A type parameter can have a default, used when nothing is given or inferred: type ApiResult<T = unknown> = .... Then ApiResult on its own means ApiResult<unknown>. Defaults work like default function parameters, but for types.
const type parameters
When a generic function receives a literal such as ["S", "M", "L"], TypeScript normally widens it to string[]. Adding const before the type parameter (TypeScript 5.0+) asks for the narrow, as const style type instead:
function options<const T extends readonly string[]>(values: T): T {
return values;
}
const sizes = options(["S", "M", "L"]); // type: readonly ["S", "M", "L"]This saves callers from writing as const themselves.
Common mistakes
- Using a type parameter only once. In
function log<T>(x: T): void,Tconnects nothing;x: unknownis simpler. Generics are for linking types, for example an input and an output. - Reaching for
anyinstead. If a function works for many types, a generic keeps the types;anyloses them. - Forgetting a constraint. Accessing
item.lengthon a bareTis an error; addT extends { length: number }. - Over-engineering. Three nested type parameters with complex constraints are hard to read. Start simple.
Key points
- Generics let one function, type or class work with many types while keeping full type checking.
- Type parameters such as
<T>are usually inferred from the arguments, but you can pass them explicitly. - Constraints (
T extends { length: number },K extends keyof T) limit what types are allowed and unlock their properties. - Type aliases, interfaces and classes can be generic, for example
ApiResult<T>orQueue<T>. - Type parameters can have defaults (
<T = unknown>), andconsttype parameters infer literal types.
Exercise
Write a generic function groupBy<T, K extends string>(items: T[], getKey: (item: T) => K): Record<K, T[]> that groups items into lists by a key. Use it to group these orders from a bakery in Pune by status: { id: 1, status: "pending" }, { id: 2, status: "delivered" }, { id: 3, status: "pending" }. Log the ids in each group.
Show solution
function groupBy<T, K extends string>(items: T[], getKey: (item: T) => K): Record<K, T[]> {
const groups = {} as Record<K, T[]>;
for (const item of items) {
const key = getKey(item);
(groups[key] ??= []).push(item);
}
return groups;
}
type Order = { id: number; status: "pending" | "delivered" };
const orders: Order[] = [
{ id: 1, status: "pending" },
{ id: 2, status: "delivered" },
{ id: 3, status: "pending" },
];
const byStatus = groupBy(orders, (o) => o.status);
console.log(byStatus.pending.map((o) => o.id)); // [ 1, 3 ]
console.log(byStatus.delivered.map((o) => o.id)); // [ 2 ]K is inferred as "pending" | "delivered", so byStatus.pending is checked and autocompleted. For plain JavaScript there is also the built-in Object.groupBy(), which returns a similar object.
Quick Quiz
1. In function firstItem<T>(items: T[]): T | undefined, what is T for firstItem([5, 9])?
TypeScript infers the type parameter from the argument.
2. What does T extends { length: number } do?
A constraint limits which types can be used for T.
3. Which call fails for pluck<T, K extends keyof T>(items: T[], key: K) with student objects that have name and marks?
"age" is not one of the keys of the student type.
4. When is a generic type parameter unnecessary?
Generics are useful when they connect types, such as an input and an output.
Learn TypeScript with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
