10 — Generics: Type Variables for Reusable Code
"Scary advanced syntax I'll learn when I need it" was my generics stance, and the deferral kept costing me. Writing it down stripped the mystery: a generic is a type variable, a placeholder for a type that gets filled in when the code is used, so one definition works with many types without losing precision or reaching for any. [1] That's the whole idea; everything else is syntax.
The problem generics solve is direct. Suppose I want a function that returns whatever I pass in — an identity function:
function identity(value: any): any {
return value;
}This works for any type, but the return type is any — I've thrown away everything the compiler knew. If I pass a string, the result is any, and calling .toUpperCase() on it isn't checked. The alternative without generics is writing identity once per type, which defeats reuse. Generics let me keep the input and output types linked:
function identity<T>(value: T): T {
return value;
}The <T> declares a type parameter named T. When the function is called, TypeScript fills in T from the argument: identity("hello") has T inferred as string, so the return type is string. The link between input and output is preserved, and I wrote exactly one function.
Type parameters and angle brackets
The syntax is a pair of angle brackets after the function (or class, or type) name, listing one or more type parameters: <T>, <T, U>, <K extends string>. By convention single-letter names (T for Type, K for Key, V for Value, E for Element) are common, but descriptive names work too. A type parameter is a variable at the type level — it stands for "some type, to be determined" [1].
The same idea scales beyond functions:
- Generic classes — class Box<T> { contents: T } — a Box<string> holds strings, Box<number> holds numbers.
- Generic types — type Pair<A, B> = { first: A; second: B } — reusable shapes parameterized by type.
- Generic interfaces — interface Repository<T> { find(id: string): T } — contracts that work for any entity type.
Wherever I'd be tempted to use any, a generic is usually the right answer.
Inference at the call site
Most of the time I don't spell out the type argument — the compiler infers it from the value passed in. identity("hello") infers T = string without me writing identity<string>("hello"). Explicit type arguments exist (identity<string>("hello")) and are useful when inference can't determine the type (e.g., when the argument doesn't carry enough information) or when I want to force a specific type. The default is to let inference work and only annotate when it can't.
Generic constraints
Unconstrained, T can be anything — which means inside the function I can only use operations that exist on every type (basically none). A generic constraint limits what T can be, so I can rely on specific properties. The extends keyword sets the constraint [2]:
function getLength<T extends { length: number }>(value: T): number {
return value.length; // safe: T is guaranteed to have length
}Now T must have a length: number property — strings, arrays, and any object with length qualify. The constraint lets me use .length inside the function, while still being generic over the specific type. Constraints are how generics stay flexible and useful: I narrow what T can be just enough to do real work, without pinning it to a single type.
The common patterns: T extends { length: number } for "anything with a length"; T extends Item for "a subtype of Item"; keyof inside constraints (<K extends keyof Obj>) for "a valid key of this object." The last one is the foundation of most type-safe property-access utilities.
How I use this
My defaults: reach for a generic the moment I'm about to write any to keep a function reusable — the generic preserves the type link that any destroys; let inference fill in type arguments at call sites and only annotate explicitly when inference is ambiguous; and add a constraint with extends the moment I need to use a property of T inside the body. The mental shift is that generics aren't advanced syntax — they're just type variables, and once that lands, reading Array<T> or Promise<T> or Map<K, V> becomes reading the type system's vocabulary for "this works for any type, precisely."
References
[1] Microsoft, "Hello World of Generics," TypeScript Handbook, 2024. [Online]. Available: https://www.typescriptlang.org/docs/handbook/2/generics.html#hello-world-of-generics
[2] Microsoft, "Generic Constraints," TypeScript Handbook, 2024. [Online]. Available: https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints
[3] Microsoft, "Generic Types," TypeScript Handbook, 2024. [Online]. Available: https://www.typescriptlang.org/docs/handbook/2/generics.html
Knowledge check · Question 1 of 5
A generic type parameter is best described as…
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!