Write logic once with full type safety — generic functions, type aliases, and classes, plus constraints with extends and keyof for type-safe utilities.
Why: a generic is a type variable filled in at each use site — write the logic once, keep full type safety for every caller. The compiler usually infers T from the arguments.
// <T> is decided by whoever calls the function
function first<T>(items: T[]): T | undefined {
return items[0];
}
const n = first([1, 2, 3]); // n: number | undefined
const s = first(['a', 'b']); // s: string | undefined
// Generic type aliases — reusable containers
type ApiResponse<T> = {
status: number;
data: T;
};
const res: ApiResponse<{ name: string }> = {
status: 200,
data: { name: 'Alice' },
};
// Generic classes
class Stack<T> {
private items: T[] = [];
push(item: T) { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
}
const stack = new Stack<number>();
stack.push(1);
stack.push(2);
// stack.push('three'); // Error: string is not a number
console.log(n, s, res.data.name, stack.pop());Why: "T extends Something" limits what T can be, so you can safely use its members inside the function. Constraining one type parameter by another (K extends keyof T) is the pattern behind most type-safe utilities.
// T must have a length — strings, arrays, anything measurable
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
console.log(longest('hello', 'hi')); // strings have length
console.log(longest([1, 2, 3], [4])); // arrays do too
// longest(10, 20); // Error: number has no 'length'
// Constrain one parameter by another
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
console.log(pluck(users, 'name')); // string[] → ['Alice', 'Bob']
console.log(pluck(users, 'id')); // number[] → [1, 2]
// pluck(users, 'age'); // Error: '"age"' is not a key