Alex Carter1 min readTypeScript Patterns I Actually Use in Production
Beyond the basics: discriminated unions, branded types, and the patterns that have earned a permanent place in my toolbox.
TypeScript has a reputation for being verbose, but most of the noise comes from patterns that aren't actually that useful. Here are the ones I reach for daily — the ones that pay for themselves.
Discriminated Unions
If you take one thing from this post, take this. Discriminated unions model state that can be in one of several shapes, and the compiler narrows it for you.
type RequestState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };Because every variant has a literal status, TypeScript narrows automatically:
function render<T>(state: RequestState<T>) {
switch (state.status) {
case "success":
return state.data; // T — narrowed correctly
case "error":
return state.error.message; // Error — narrowed correctly
default:
return null;
}
}Why it matters
Exhaustive switch checks mean the compiler errors if you add a new variant
and forget to handle it. That's refactoring safety for free.
Branded Types
Sometimes two types are structurally identical but semantically different — a
UserId and a PostId are both strings, but mixing them up is a bug.
type Brand<T, B> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type PostId = Brand<string, "PostId">;
function getUser(id: UserId) { /* ... */ }
const post: PostId = "abc" as PostId;
// getUser(post); // ❌ Type error — exactly what we wantThe satisfies Operator
as const is too strict; : Type is too loose. satisfies is just right.
const config = {
port: 3000,
host: "localhost",
} satisfies ServerConfig;
// config.port is still `number`, not `3000`Generic Constraints Done Right
Avoid any. Use bounded generics to express intent.
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
return keys.reduce((acc, key) => {
acc[key] = obj[key];
return acc;
}, {} as Pick<T, K>);
}Summary
- Discriminated unions for state machines.
- Branded types to stop accidental mix-ups.
satisfieswhen you want validation without losing narrow types.
These patterns keep types precise without getting in the way. Add them one at a time — you'll feel the difference.
// Comments
Powered by Giscus
Comments are powered by GitHub Discussions. Configure Giscus incomponents/blog/comments.tsxto enable live discussions.
// Related Posts
How I Ship Side Projects Without Burning Out
A sustainable system for building side projects: scoping, timeboxing, and knowing when something is 'done'.
2026-05-18Getting Started with Next.js 14 App Router
A practical tour of the App Router: server components, layouts, data fetching, and the mental models that make it click.
2026-01-10Mastering React Server Components: A Mental Model
React Server Components sound complicated, but they click once you grok the module boundary. Here's the mental model that finally made it stick.