TypeScript Patterns That Carry Their Weight in 2026
Discriminated unions, satisfies, branded types, and template literals — the handful of TypeScript features that catch real bugs, and the clever ones I've learned to leave alone.
The short answer: The TypeScript patterns that carry their weight in 2026 are a small set: discriminated unions to make illegal states unrepresentable, satisfies to keep inference while checking a value against a type, branded types for IDs and units you don't want to mix up, and template literal types for string shapes. They catch real bugs and pay for their complexity — the baroque conditional-type gymnastics mostly don't, so I leave those alone.
TypeScript has grown enormously powerful, and with that power comes the temptation to write types that are impressive to read and miserable to maintain. After years of it, I've settled on a small set of patterns that reliably catch real bugs and pay for their complexity — plus a shorter list of clever tricks I now deliberately avoid. Here's the working set.
Discriminated unions make illegal states unrepresentable
This is the one I'd keep if I could keep only one. Most "optional field" bugs come from a type that allows combinations that can't actually happen — isLoading: true and a populated data, say. A discriminated union makes those combinations impossible to write.
You can't accidentally read data in the error branch — it isn't there. Whole classes of defensive ?. and if (data) checks disappear, and the compiler walks you through every case.
satisfies gives you both inference and checking
Before satisfies you had to choose: annotate a value (and lose precise inference) or leave it inferred (and lose the guarantee it matches a shape). satisfies gives you both — it validates against the type while keeping the narrow inferred type.
Branded types for values that look alike
A UserId and an OrderId are both strings, and the compiler will happily let you pass one where the other belongs. Branding makes them distinct without any runtime cost.
I reserve this for identifiers and units (cents vs dollars, seconds vs ms) where a mix-up is a real, costly bug — not for every string in the app.
The clever ones I leave alone
Deeply recursive conditional types, giant template-literal type machines that parse strings at the type level, inference gymnastics that produce an error message no human can read — I've written them, admired them, and then watched a teammate (or future me) stall for an hour trying to extend them. If a type takes longer to understand than the code it guards, it's a net loss. The goal is fewer runtime bugs, not a type-level proof of cleverness.
The rule underneath
Reach for a type feature when it makes an invalid state impossible or a real mix-up a compile error. Stop when the type becomes a puzzle. The best TypeScript, like the best code, is the kind the next person can read without a running start — it just happens to also refuse to compile when you're about to ship a bug.