The Day TypeScript Generics Finally Clicked
For two years I copy-pasted angle brackets without understanding them. Then one refactor made the whole mental model snap into place — generics are functions, but for types.
The short answer: Generics finally clicked for me when a teammate framed them in one line: they're just functions that take types instead of values. A regular function takes a value and returns a value; a generic takes a type parameter like <T> and returns a type, so the type flows in from your argument and back out of your return. Once you read <T> as a parameter living in type-space, the angle brackets stop being noise.
I used generics for about two years before I actually understood them. I'd see Array<string> and Promise<User> and useState<number>(0) and I'd pattern-match: angle brackets, put a type in, carry on. When I had to write a generic myself I'd flail, add <T> somewhere, let the red squiggles guide me by trial and error, and eventually land on something that compiled. I couldn't have explained why it worked. Then one afternoon, mid-refactor, it clicked — and I've never looked at a type the same way since.
The sentence that fixed my brain
The refactor was boring: a getFirst helper that returned the first element of an array. I'd written it returning any, which meant every caller lost its types the moment they touched the result. A teammate's review comment was one line: "generics are just functions that take types instead of values."
That's it. That's the whole insight. A regular function takes a value in and gives a value back. A generic takes a type in and gives a type back. The <T> is a parameter, exactly like (x) is a parameter — it just lives in type-space.
I didn't tell TypeScript what T was. It inferred it from the argument I passed, the same way a function doesn't need you to announce that 5 is a number. The type "flowed through." Once I saw it as a value flowing through a pipe, every confusing generic I'd ever copied suddenly had an obvious shape.
Constraints are just extends
The next thing that had always scared me was T extends something. It reads like inheritance, which sent my brain down the wrong path entirely. It's not inheritance — it's a guard on the type parameter. It says "T can be anything, as long as it has this shape." It's the type-world version of validating an argument before you use it.
K extends keyof T was the exact thing I used to copy without understanding. Now it reads plainly: K is a key, but constrained to the real keys of T. And T[K] — indexed access — gives back the type of that specific property. The compiler tracks it per-call. The first time I watched pluck(user, "admin") resolve to boolean on its own, I actually said "oh" out loud.
A real utility I reach for
The payoff is that you can build small, honest helpers that stay fully typed across your whole app. Here's one I still use — grouping a list by some key, with the result typed as a record keyed by that property's value.
No any, no casting at the call site, no lost autocomplete. The caller passes a value and a key; the types come along for free.
What I'd tell past me
Stop thinking of <T> as a magic incantation and start reading it as a parameter list. A generic is a function; its arguments happen to be types; the compiler infers them the same way it infers values. extends is a guard, not inheritance. Once those three sentences are in your head, the angle brackets stop being noise and start being the most expressive tool in the language. It took me two years to hear it. It'll take you one blog post.