Generics

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.

The question this chapter answers

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:

TypeScript — Try it Yourself
Loading editor…
Type check + Console

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":

TypeScript — Try it Yourself
Loading editor…
Type check + Console

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:

TypeScript — Try it Yourself
Loading editor…
Type check + Console

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.

Everyday analogy

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:

TypeScript — Try it Yourself
Loading editor…
Type check + Console

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.

History changed here

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.

The engineer's notebook

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.

Real-world example — chika.ng Academy

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 any when you meant a generic. If the type is known to the caller, a generic carries it through safely. any throws 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 infers T = string from the argument. Supply it explicitly only when inference genuinely can't.
  • A type parameter used exactly once. function log<T>(x: T): void relates nothing — T appears once and vanishes. That's just unknown (or any) with extra ceremony. A generic earns its keep by connecting two or more positions.
  • Confusing the declaration with the use. In Box<T>, T is the parameter (declared); in Box<number>, number is the argument (supplied). Mixing them up — writing Box<T> where you meant Box<number> — leaves you with an unbound T.
  • Assuming a generic constrains anything. Bare <T> accepts every type, so inside the function you can't assume T has a .length or an .id. Narrowing that is the job of constraints — a later chapter — not of the plain type parameter.
Thought experiment

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

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Junior
"What is a generic in TypeScript, and what does the <T> mean?"
Model answer — "A generic lets you write a function or type over a type you don't fix in advance. <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."
Follow-up they'll throw: "How is that different from any?" → any forgets the type — it's a hole in the checker; a generic remembers and relates it, so downstream code stays fully typed.
Intermediate
"Why choose a generic over any or unknown, and how does inference fit in?"
Model answer — "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."
Follow-up: "When is 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.
Senior
"You're building the shared data layer for a large app. How do generics shape that design, and what's the failure mode if you skip them?"
Model answer — "I'd make the boundary generic — one 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."
The senior move: frame generics as an invariant encoded once at a boundary, then reach past syntax to how it holds a large team's code together as the schema evolves.
Company lens — how the same topic gets asked
micro1Live and conversational — the AI interviewer wants you to reason out loud. State what a generic is, then contrast it with any in the same breath; naming the trade-off unprompted reads as fluency.
GoogleWants precision and "can you do better?" Expect them to push from a duplicated function to the generic, then ask you to justify inference vs. explicit type arguments exactly.
MicrosoftMicrosoft created TypeScript — expect depth. Be ready to explain inference, the declaration-vs-argument distinction, and why T used once is a smell. Cite the built-ins (Array<T>, Promise<T>).
Amazon / AWSOperational lens: how a generic API layer reduces bugs and duplication at scale. Tie it to maintainability across many services and schema changes, not just the syntax.
Hands-on exercise
  1. In the editor above, call identity with an object literal like { id: 1, title: "Intro" } and hover the result. Confirm TS inferred the full object type — not any, and not something you had to write by hand.
  2. Add a generic last<T>(arr: T[]): T | undefined beside first. Call it on a number[] and a string[] and check each result type. Then try calling first([]) on an empty array — what is T, and how would you supply it explicitly?
  3. Take the firstAny example, change it to the generic first<T>, and watch the .toUpperCase() mistake turn from a silent runtime crash into a red compile error.
  4. 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): T captures 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> and first<T> thread the element type all the way through.
  • any forgets a type, unknown withholds 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 like TData win 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.
Narrative bridge

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 any at 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.