TypeScript course menu
TypeScript Generics: Reusable, Type-Safe Functions, Types and Classes
Learn TypeScript generics from scratch: why any loses information, generic functions with type parameters, type argument inference, generic types and interfaces, generic classes, multiple type parameters, and the generics you already use such as Array<T> and Promise<T>.
On this page
Generics let you write code that works with many types while still keeping track of which type is being used. They are how Array<string>, Promise<User> and Map<string, number> work, and they are essential for writing reusable functions and data structures.
The problem#
Here is a function that returns the first item of an array. How should it be typed?
function firstAny(items: any[]): any {
return items[0];
}
const n = firstAny([10, 20]); // type: any
console.log(n.toUpperCase?.()); // no error from TypeScript, even though n is a numberundefinedWith any, the function accepts everything but loses the type: the caller gets any back, and all checking stops. Writing a separate function for each type (firstNumber, firstString) would be absurd.
A generic function#
A type parameter, written in angle brackets, acts like a variable for a type:
function first<T>(items: T[]): T | undefined {
return items[0];
}
const n = first([10, 20]); // T is number, so n: number | undefined
const s = first(["a", "b"]); // T is string, so s: string | undefined
console.log((n ?? 0) + 1, s?.toUpperCase());11 A<T>declares a type parameter calledT(any name works;Tfor "type" is conventional).items: T[]and the return typeT | undefinedboth refer to it.- When you call
first([10, 20]), TypeScript infersT = numberfrom the argument, so the result is typed correctly.
The type is preserved, and mistakes are still caught:
function first<T>(items: T[]): T | undefined {
return items[0];
}
const n = first([10, 20]);
n?.toUpperCase();error TS2339: Property 'toUpperCase' does not exist on type 'number'.You can also pass the type explicitly: first<string>(["a"]). Usually inference is enough.
More generic functions#
function last<T>(items: readonly T[]): T | undefined {
return items.at(-1);
}
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
function mapValues<T, R>(items: T[], fn: (item: T) => R): R[] {
return items.map(fn);
}
const lengths = mapValues(["html", "css"], (word) => word.length); // number[]
console.log(last([1, 2, 3]), pair("id", 42), lengths);3 [ 'id', 42 ] [ 4, 3 ]Multiple type parameters (A, B, or T and R for "result") let you relate several types. In mapValues, the callback's parameter word is inferred as string and its return type number becomes R.
Generic types and interfaces#
Types can have type parameters too:
interface ApiResponse<T> {
ok: boolean;
data: T;
fetchedAt: string;
}
type Pair<K, V> = { key: K; value: V };
const userResponse: ApiResponse<{ id: number; name: string }> = {
ok: true,
data: { id: 1, name: "Asha" },
fetchedAt: "2026-10-24",
};
const setting: Pair<string, boolean> = { key: "darkMode", value: true };
console.log(userResponse.data.name, setting.value);Asha trueGeneric classes#
class Stack<T> {
#items: T[] = [];
push(item: T): void {
this.#items.push(item);
}
pop(): T | undefined {
return this.#items.pop();
}
get size(): number {
return this.#items.length;
}
}
const history = new Stack<string>();
history.push("/home");
history.push("/courses");
console.log(history.pop(), history.size);/courses 1class Stack<T> {
#items: T[] = [];
push(item: T): void {
this.#items.push(item);
}
}
const history = new Stack<string>();
history.push(404);error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.Generics you already use#
const scores = new Map<string, number>([["asha", 92]]);
const loaded: Promise<string[]> = Promise.resolve(["HTML", "CSS"]);
console.log(scores.get("asha"), (await loaded).length);92 2When to use generics#
Use a type parameter when it connects two or more places: an argument and the return value, two arguments, or a property and a method. If a type parameter appears only once, you probably do not need it; a plain type (or unknown) is simpler.
Practice#
Practice exercise
Write a generic function groupBy<T, K extends string>(items: T[], getKey: (item: T) => K): Record<K, T[]> (the extends string part is explained in the next lesson), and use it to group students by grade.
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;
}
const students = [
{ name: "Asha", grade: "A" },
{ name: "Ravi", grade: "B" },
{ name: "Meera", grade: "A" },
];
const byGrade = groupBy(students, (s) => s.grade);
console.log(byGrade.A.map((s) => s.name), byGrade.B.length);[ 'Asha', 'Meera' ] 1{} as Record<K, T[]> starts with an empty object asserted to the final type, because it is filled in by the loop.
Key takeaway#
Finished this lesson?