Reading — step 1 of 7
Learn
Generics let a function or type work with multiple kinds of values WITHOUT using any. They preserve type information through your code — write a function once, get full type checking for every caller.
The motivating problem
Imagine writing first for arrays — a function that returns the first element:
// Wrong: returns any — caller loses type info
function first(arr: any[]): any {
return arr[0];
}
const n = first([1, 2, 3]); // n: any — not number!
n.foo(); // no error, but crashes at runtime
any defeats the type checker. Could we write specific versions for every type? firstNumber, firstString, etc — that's absurd. Generics solve this:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // n: number | undefined
const s = first(["a", "b"]); // s: string | undefined
Type parameters — <T>
<T> introduces a type parameter. Read it as: "for some type T..." The compiler infers T from how the function is called — you rarely write it explicitly.
Multiple type parameters:
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const p = pair("Alice", 30); // p: [string, number]
Names T, U, K, V are conventional. Use longer names when meaning helps: <Item>, <Key, Value>.
Generic interfaces
interface Box<T> {
value: T;
}
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: "hi" };
Generic types are reusable shapes parameterized by type. The standard library is full of them: Array<T>, Promise<T>, Map<K, V>, Set<T>.
Generic constraints — extends
Sometimes you need T to have certain shape:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("hello", "hi"); // OK — strings have length
longest([1, 2, 3], [4]); // OK — arrays have length
longest(42, 5); // ✗ compile error — numbers don't have length
T extends X constrains T to types that include X's shape.
Default type parameters
interface ApiResponse<T = unknown> {
status: number;
data: T;
}
const raw: ApiResponse = { status: 200, data: "anything" }; // T defaults to unknown
const typed: ApiResponse<User> = { status: 200, data: user };
Defaults make a generic friendlier when callers don't care.
Generic classes
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]; }
}
const s = new Stack<number>();
s.push(1);
s.push(2);
const top = s.peek(); // top: number | undefined
Generic classes let you build typed data structures once.
When NOT to over-generic
Generics are a hammer. Don't make every function generic — most code works with concrete types. Reach for generics when:
- The function works on collections of any type (sort, find, filter helpers).
- You're building a reusable data structure or wrapper.
- A factory or builder needs to preserve input → output types.
For a function that always handles User, just type it as User. Generics are for genuine polymorphism.
Common mistakes
- Using
anyinstead ofT— defeats the type checker. Generics keep types flowing. - Type parameter without constraint when needed —
function first<T>(x: T) { return x.length; }fails because T isn't constrained to have a length. - Specifying T explicitly when inference works —
first<number>([1, 2, 3])is noise. Justfirst([1, 2, 3]).
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…