Generics are type parameters. function first<T>(items: T[]): T | undefined works for arrays of numbers, strings or students — and the return type follows whatever goes in, instead of collapsing to any.
Constraints limit what a type parameter may be: <T extends { id: number }> accepts any object that has a numeric id.
Generic types are common for wrappers such as API responses (ApiResult<T>), caches and repositories.
function first<T>(items: T[]): T | undefined {
return items[0];
}
function groupBy<T, K extends string | number>(items: T[], key: (item: T) => K): Record<K, T[]> {
const groups = {} as Record<K, T[]>;
for (const item of items) {
const k = key(item);
(groups[k] ??= []).push(item);
}
return groups;
}
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find((item) => item.id === id);
}
type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string };
function wrap<T>(data: T): ApiResult<T> {
return { ok: true, data };
}
const people = [
{ id: 1, name: "Amina", form: 4 },
{ id: 2, name: "Juma", form: 3 },
{ id: 3, name: "Neema", form: 4 },
];
const n = first([10, 20, 30]); // number | undefined
console.log(n, findById(people, 2)?.name);
console.log(groupBy(people, (p) => p.form));
const result = wrap(people.length);
if (result.ok) console.log("Count:", result.data);Key points
- Use a generic when the output type depends on the input type.
extendsconstrains a type parameter to shapes you can rely on.- Don't add generics that are used only once — they add noise without safety.
Exercise
Write a generic Stack<T> (as a class or functions) with push, pop and peek. Then write uniqueBy<T>(items, key) that removes duplicates using a key function.
Show solution
Try the exercise yourself first — then compare your approach with this one.
A generic class Stack<T> stores items of one type and returns that same type from pop and peek. uniqueBy remembers the keys it has already seen in a Set, keeping only the first item for each key.
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
}
function uniqueBy<T, K>(items: T[], key: (item: T) => K): T[] {
const seen = new Set<K>();
return items.filter((item) => {
const k = key(item);
if (seen.has(k)) return false;
seen.add(k);
return true;
});
}
const history = new Stack<string>();
history.push("Home");
history.push("Lessons");
console.log(history.pop(), history.peek(), history.size); // Lessons Home 1
const entries = [
{ id: 1, name: "Amina" },
{ id: 2, name: "Juma" },
{ id: 1, name: "Amina (duplicate)" },
];
console.log(uniqueBy(entries, (e) => e.id).map((e) => e.name)); // [ 'Amina', 'Juma' ]