AcademyTypeScript
Generics: a type the caller fills in
What a type parameter actually does, why extends means constrain rather than inherit, and the rule that tells you when a generic is earning its keep.
3 min readUpdated 3 exercises
The relationship you could not otherwise state
Without generics, a function that returns what it was given has to lie:
function first(items: any[]): any { return items[0]; }
const name = first(['a', 'b']); // any. The compiler has stopped helping.A type parameter lets you say this comes back as whatever went in:
function first<T>(items: T[]): T | undefined { return items[0]; }
first(['a', 'b']); // string | undefined
first([1, 2]); // number | undefinedT is not a type. It is a hole the caller fills, usually without noticing — TypeScript infers it from the argument. You only pass one explicitly when there is nothing to infer from.
extends means "at least this", not "inherits from"
The keyword is reused and it misleads everyone once. In a type parameter, extends is a constraint: it restricts what the caller may pass.
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest('abc', 'de'); // string
longest([1, 2], [3]); // number[]
longest(1, 2); // Error: number has no 'length'Without the constraint, a.length would not compile — inside the function, an unconstrained T could be anything, so you can do nothing with it. That is the trade generics make: the more you constrain, the more you can do in the body, and the fewer callers you serve.
Two mistakes worth recognising
A type parameter used once. If T appears in only one position, it is doing nothing a plain type could not:
function log<T>(value: T): void { console.log(value); }
// T is used once. This is just (value: unknown) => void.A generic earns its place by relating two things: an argument to a return value, one argument to another, a key to the type of that key.
Returning a type the caller chose. This compiles and is almost always wrong:
function parse<T>(json: string): T {
return JSON.parse(json); // T is whatever the caller claimed
}
const user = parse<User>(raw); // no checking happened anywhereThat is as with extra ceremony. Return unknown and make the caller narrow, or validate and return a real type.
Defaults, and inference you can steer
interface Paged<T, Meta = { total: number }> {
items: T[];
meta: Meta;
}
type Users = Paged<User>; // Meta falls back to { total: number }And when you want a literal type instead of the widened one, constrain to the primitive:
function pick<T extends string>(value: T): T { return value; }
pick('idle'); // 'idle' — not stringThe constraint is what keeps the literal: T extends string infers as narrowly as it can, where a bare T widens.
| You want | Write |
|---|---|
| Whatever came in, unchanged | <T>(x: T) => T |
| Only things with a property | <T extends { id: string }> |
| A key of an object you were given | <T, K extends keyof T> |
| A literal, not the widened type | <T extends string> |
| A fallback the caller may override | <T = string> |
The rule
Reach for a generic when two types in a signature must agree, not when you want a function to accept several shapes — a union does that more simply and reads better in the error message. A good generic is invisible at the call site: the caller passes an argument and gets back a precise type without ever writing an angle bracket.
Now practice it
2 challenges · 1 quizReading this page is the cheap half. These are the exercises that make you use it.
- Generics — Identity, Constraint & DefaultProThe identity function, then a constraint, then a default — the three moving parts of a type parameter.ChallengeSolve · EasyChallengeSolve · Easy
- Generic Stack<T>ProA Stack<T> where push and pop have to agree, which is the case a generic exists for.ChallengeSolve · MediumChallengeSolve · Medium
- Generics & Utility Types · Generic BasicsGeneric basics, including what an unconstrained type parameter lets you do inside the function.QuizTake · EasyQuizTake · Easy
Read next
Deriving types with keyof and typeof
The three operators that read types out of things you already wrote, so a type never has to be maintained next to the value it describes.
3 min
Partial, Pick, Omit and the rest
Partial, Pick, Omit, Record, Required, Readonly and the rest — what each one is for, which one is unsafe, and how to stop re-declaring the same shape.
3 min