TypeScript generics that make your APIs self-documenting
How I use generics, discriminated unions, and branded types to turn a TypeScript API layer into documentation that can't go stale.
Comments rot, types don't
A comment saying 'id must be a valid UUID' is a suggestion. A branded type is a guarantee. On every TypeScript codebase I ship now, primitive-obsessed fields — user IDs, email addresses, currency amounts — get wrapped in branded types so the compiler rejects a raw string where a validated one is required.
This single change eliminates an entire category of bugs: passing an order ID where a customer ID was expected, or a dollar amount where cents were expected. The type system becomes the documentation, and it's impossible for it to go out of sync with the code.
Discriminated unions instead of optional-field soup
A response shape with five optional fields that 'depend on status' is a runtime bug waiting to happen. I model API responses as discriminated unions keyed on a `status` or `type` literal, so TypeScript narrows the shape automatically inside every `if` or `switch` branch.
- Every union member has only the fields that are actually valid for that state
- Exhaustiveness checks (`never` in the default case) catch missing branches at compile time
- Frontend and backend can share the same union types via a shared package
- This turns 'what fields exist when status is pending?' into a question the compiler answers for you
Generic repository and service layers
Instead of writing a near-identical CRUD service for every Mongoose or Prisma model, I write one generic `Repository<T>` with typed `find`, `create`, and `update` methods. Model-specific services extend it and add only the domain logic that's actually unique.
The payoff compounds: adding a new entity to the system means writing a schema and a thin service, not 200 lines of boilerplate. Refactors that touch every repository become one generic-type change instead of a search-and-replace across dozens of files.
Inference-friendly function signatures
Generics are only useful if callers never have to write them out by hand. I design functions so TypeScript infers the generic parameter from the arguments — a validation function that takes a Zod schema, for example, infers its return type from that schema automatically.
The test I apply to every exported function: can someone call it with sensible arguments and get full autocomplete on the result, without ever typing an angle bracket? If not, the generic is designed wrong.
Where strict typing stops paying off
Not everything needs a branded type or a five-level generic. Internal one-off scripts, quick prototypes, and glue code between well-typed layers are fine as plain types. Over-engineering the type layer slows down iteration without preventing any bugs that would actually occur.
The rule I follow: invest in types at trust boundaries — API inputs, database reads, third-party responses — where invalid data actually enters the system. Everywhere else, keep it simple.
Written by
Tariq Mehmood
Full Stack MERN Developer


