Lesson 1 of 8
Generics Fundamentals
"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system." — Andrew Hunt & David Thomas, The Pragmatic Programmer
So far you have learned to describe concrete shapes: a User, a Course[], a Result with a kind tag. Give TypeScript enough shapes and it can model anything your program touches. But a nagging pattern keeps appearing — the same logic, written again and again, differing only in the type it carries. A function that returns the first element of an array of numbers is identical to the one for strings, and to the one for users, except for one word. Copy it per type and you drown in duplication; erase the type with any and you throw away the very safety you came to TypeScript for. Generics are the third way: write the logic once, over a type you don't name yet, and let the compiler fill it in — differently — at every call. This is the feature that separates a codebase that scales from one that rots, and it is where TypeScript stops feeling like "JavaScript with labels" and starts feeling like a type system.
You have one algorithm that should work over many types — first-of-array, a box, a cache, an API fetch. How do you write it once without either duplicating it per type or falling back to any and losing every guarantee?
Core Concepts
The problem: duplicate, or lose safety
Say you want the first element of an array. Without generics you have two bad options. The first is to write the function once per type — pure duplication that drifts out of sync the moment one copy changes:
function firstNumber(arr: number[]): number | undefined { return arr[0]; }
function firstString(arr: string[]): string | undefined { return arr[0]; }
function firstUser(arr: User[]): User | undefined { return arr[0]; }
// …one more for every type you ever hold. This does not scale.The second is to write it once with any — and watch the return type evaporate. The function compiles for every input, but it hands back a value the compiler no longer understands:
That second console.log is the tragedy. any returned a hole: calling a string method on a number sails past the compiler and blows up in production. You wrote one function, but you paid for it with the entire point of TypeScript.
Type parameters: a placeholder for a type
A generic adds a type parameter — a placeholder for a type, written in angle brackets — that the caller (or the compiler) fills in. The canonical example is the identity function, which returns its argument unchanged. Its type isn't "takes anything, returns anything"; it's the far stronger claim "whatever type goes in, that exact type comes out":
T is not a real type — it's a variable that ranges over types, bound fresh at each call. Two things make this powerful. First, inference: you rarely write the angle brackets, because TypeScript reads T off the argument — identity("hi") infers T = string on its own. Second, explicit supply: when inference can't see enough (an empty array, a wider type than the literal), you name it — identity<number>(1) — and the compiler holds you to it. Either way, first(numbers) gives back number | undefined, first(strings) gives back string | undefined. One function, and the element type rides all the way through.
Generic over containers
The same placeholder works on types, not just functions. A Box<T> is a container whose contents you name at the point of use — the type parameter threads from the declaration into every field:
Box<number> and Box<string> are two distinct, fully-checked types minted from one declaration. That is the whole promise of generics scaled up from functions to data structures — and it's exactly how the standard library is built.
A generic is a cookie cutter. The cutter's shape is fixed — that's your logic — but it stamps whatever dough you feed it: sugar, gingerbread, shortbread. Box<T> is the cutter; Box<number> and Box<User> are the cookies. You don't carve a new cutter per flavour (duplication), and you don't stamp a shapeless blob and hope (any). One cutter, many typed cookies — each one the exact shape you asked for.
Generics vs any vs unknown
These three are the usual suspects when a type isn't fixed up front, and interviewers love the distinction because it exposes whether you actually understand the type system. The one-word summary: any forgets, unknown withholds, a generic remembers and relates.
| Approach | The type relationship | Downstream safety |
|---|---|---|
any |
Forgotten — it becomes a hole in the checker | None. Anything compiles; crashes surface at runtime. |
unknown |
Preserved but opaque — "some type, unspecified" | Safe, but you must narrow before you can use it. |
T (generic) |
Remembered and connected — input type is output type | Full. The caller's exact type flows through, checked end to end. |
any and a generic can look interchangeable at the boundary — both accept anything — but they behave like opposites downstream. Here is the generic version of the earlier failure. Same one function, but this time the type is remembered, so the bug is caught before it ships:
The any version compiled this and detonated at runtime; the generic version paints it red in your editor. Reach for unknown when a value's type is genuinely unknowable at that point (a JSON.parse result, a caught error) and you'll narrow it later. Reach for a generic when the type is known — to the caller — and you want to carry it through your code intact.
The mental model: a type-level function
Here is the idea that makes everything else click: a generic is a function that runs at the type level. An ordinary function takes values and returns a value. A generic takes types and returns a type. identity<T> is a machine you feed the type string and it hands you back the type (x: string) => string. Box<T> is a machine you feed number and it hands you back the object type { value: number }. You already think in functions — value in, value out. Generics are the same reflex one level up: type in, type out. Once you see them that way, the syntax stops being decoration and becomes what it is — parameters, for types.
The idea is parametric polymorphism — code parameterised by type — traced to Christopher Strachey in 1967 and made practical by the ML language in the 1970s. It reached the mainstream slowly: Java added generics only in 2004, C# in 2005. That C# design was led by Anders Hejlsberg — who then went on to create TypeScript, shipping generics in version 0.9 (2013). So TypeScript's generics aren't an afterthought bolted onto JavaScript; they're a battle-tested idea carried in by the person who'd already built them once before. That lineage is why they feel so complete.
Conventions, and built-ins you already use
The single-letter names are a decades-old convention, not a rule: T for the primary Type, U for a second, K for a Key, V for a Value, E for an Element or Error. They're fine for small, obvious generics like identity<T>. When a type has several parameters or the meaning isn't self-evident, a descriptive name reads far better — Map<TKey, TValue> or ApiResponse<TData> tells the next reader what belongs where. Clarity beats tradition.
And here's the reassuring part: you've been using generics since Volume 1 without the name. Array<T> (that's what number[] desugars to), Promise<T> (an async function returns Promise<string>), Map<K, V>, Set<T>, Record<K, V> — every one is generic. When you wrote new Map<string, User>(), you supplied two type arguments explicitly. Generics aren't a new dialect; they're the grammar the standard library was already written in.
How this shows up in day-to-day work:
• Let inference do the work. Write first(items), not first<User>(items) — supply the type argument only when TS can't figure it out (empty collections, deliberate widening).
• "Am I copy-pasting a function and swapping one type?" is the tell that a generic is waiting to be extracted.
• One type parameter, used once, is a smell. If T appears in the signature only once it isn't relating anything — you probably wanted unknown.
• Name the payload at the call site. A single apiGet<T> beats fifty hand-written fetch wrappers.
The Academy (a Next.js + Drizzle/MySQL + TypeScript app) talks to its API through one helper: apiGet<T>(path: string): Promise<T>. Call apiGet<Course[]>("/courses") and the awaited value is typed Course[]; call apiGet<Enrollment>("/me/enrollment") and you get an Enrollment — one function, every endpoint, the caller naming the payload. Without generics the team would write a separate wrapper per model (duplication that rots the moment a route changes) or return any and re-validate every field by hand at every call. The generic captures the one true relationship — "the type you ask for is the type you get back" — and never lets it drift.
Common Mistakes
- Reaching for
anywhen you meant a generic. If the type is known to the caller, a generic carries it through safely.anythrows that knowledge away and hands you a runtime crash instead of a compile error. - Writing the type argument the compiler would infer.
identity<string>("hi")is noise — TS infersT = stringfrom the argument. Supply it explicitly only when inference genuinely can't. - A type parameter used exactly once.
function log<T>(x: T): voidrelates nothing —Tappears once and vanishes. That's justunknown(orany) with extra ceremony. A generic earns its keep by connecting two or more positions. - Confusing the declaration with the use. In
Box<T>,Tis the parameter (declared); inBox<number>,numberis the argument (supplied). Mixing them up — writingBox<T>where you meantBox<number>— leaves you with an unboundT. - Assuming a generic constrains anything. Bare
<T>accepts every type, so inside the function you can't assumeThas a.lengthor an.id. Narrowing that is the job of constraints — a later chapter — not of the plain type parameter.
You write identity<T>(x: T): T. How many distinct, fully type-checked functions does that one line represent? The honest answer is unbounded — one for every type that exists or will ever be defined, including types nobody has written yet. Now compare that to the duplication approach, where each function you want must be typed out by hand in advance. A generic doesn't just save keystrokes; it produces functions for types that don't exist at the moment you write it. That is the difference between a stencil and a photograph — one describes infinitely many pictures, the other captures exactly one.
Interview Corner
<T> mean?"<T> declares a type parameter — a placeholder that's bound to a real type at each call. In identity<T>(x: T): T it captures the relationship 'the output type is the input type,' so identity("hi") returns a string and identity(1) returns a number."any?" → any forgets the type — it's a hole in the checker; a generic remembers and relates it, so downstream code stays fully typed.any or unknown, and how does inference fit in?"any disables checking and unknown forces narrowing at every use — both discard the caller's knowledge of the type. A generic preserves and connects types across the signature, so a value passed in comes back as the same type with no re-checking. And it's cheap: TypeScript infers the type argument from the arguments, so you write first(items) and only supply <T> explicitly when inference can't see enough — like an empty array."unknown actually the right call?" → When the type is genuinely unknowable at that point — a JSON.parse or a caught error — where there's no caller-side type to carry.apiGet<T>(path): Promise<T>, one Result<T, E>, one cache keyed by type — so every consumer names its payload at the call site and gets end-to-end type flow for free. The parameter captures the single invariant 'the shape you request is the shape you receive,' which can't drift out of sync the way hand-written per-model wrappers do. Skip generics and you get one of two rots: dozens of near-identical wrappers that diverge on the next schema change, or an any-typed boundary that quietly turns a strict codebase back into JavaScript. The generic is what lets one small, correct abstraction serve a hundred call sites."any in the same breath; naming the trade-off unprompted reads as fluency.T used once is a smell. Cite the built-ins (Array<T>, Promise<T>).- In the editor above, call
identitywith an object literal like{ id: 1, title: "Intro" }and hover the result. Confirm TS inferred the full object type — notany, and not something you had to write by hand. - Add a generic
last<T>(arr: T[]): T | undefinedbesidefirst. Call it on anumber[]and astring[]and check each result type. Then try callingfirst([])on an empty array — what isT, and how would you supply it explicitly? - Take the
firstAnyexample, change it to the genericfirst<T>, and watch the.toUpperCase()mistake turn from a silent runtime crash into a red compile error. - Say it out loud, as in a live screen: explain in one breath why a generic "remembers" a type while
any"forgets" it — and why that difference is the whole reason generics exist.
Chapter Summary
- Generics let you write one reusable, type-safe thing that works over many types — the alternative to duplicating code per type or erasing it with
any. - A type parameter (
<T>) is a placeholder for a type, bound fresh at each call;identity<T>(x: T): Tcaptures the relationship "input type = output type." - TypeScript usually infers the type argument from the arguments; supply it explicitly (
identity<number>(1)) only when inference can't see enough. - The same placeholder types data, not just functions:
Box<T>andfirst<T>thread the element type all the way through. anyforgets a type,unknownwithholds it until you narrow, a generic remembers and relates it — full safety with no re-checking.- The mental model is a type-level function: types go in, a type comes out. Conventions (
T,U,K,V,E) are habit; descriptive names likeTDatawin when clarity matters. - You've used generics all along —
Array<T>,Promise<T>,Map<K, V>are the standard library written in this exact grammar.
We've met the type parameter and watched inference bind it — but only on the simplest possible function, one that just hands its argument back. Real generic functions do work: they map, filter, merge, and combine several type parameters at once, and the ways TypeScript infers T get subtler and far more useful. Next, in Generic Functions, we push past identity into functions that actually earn their type parameters.
Editorial reflection — generics are the first idea in this Volume that asks you to think one level up — not "what type is this value?" but "what is the relationship between these types?" That shift feels abstract for about a day, and then it becomes the way you see code. The engineers who never make the jump keep reaching for
anyat every boundary and calling it pragmatism; the ones who do write a single small abstraction that a whole team leans on safely for years. The syntax is five characters of angle brackets. The mindset is the entire craft.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.