The Problem
Runtime errors don't come from nowhere — they come from types that are too loose. `string | undefined` everywhere, union-busting `as` casts, and `any` escape hatches that silently spread through a codebase.
The Solution
Five TypeScript patterns eliminate entire categories of runtime errors: discriminated unions model state correctly, branded types prevent ID confusion, Zod validates at boundaries, const assertions lock down literals, and satisfies checks type without widening.
Discriminated Unions
The most powerful TypeScript pattern for modelling state. Instead of optional fields that may or may not exist, each variant of your union carries exactly the data it needs.
type Result<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };Branded Types
Prevent passing a `UserId` where a `PostId` is expected, even though both are strings. Brands make the type system enforce your domain semantics.
Adopting These Patterns
Replace optional fields with discriminated unions
Instead of `{ status: string; data?: T; error?: string }`, use a union type per state. TypeScript now knows exactly what's available in each branch.
Brand your ID types
Create `type UserId = string & { readonly __brand: 'UserId' }`. Passing a `PostId` where a `UserId` is expected is now a compile-time error, not a runtime mystery.
Add Zod at every boundary
Parse external data (API responses, URL params, form inputs) with Zod schemas at the point of entry. Trust nothing from outside your type system.
Enable strict mode in tsconfig
Add `"strict": true` to tsconfig.json. Fix every error it surfaces — each one is a latent bug. Don't silence with `as` or `!`.
Key Takeaways
- Discriminated unions eliminate impossible state by making invalid combinations unrepresentable
- Branded types enforce domain semantics the compiler can't otherwise see
- Zod bridges the gap between runtime data and TypeScript's static type system
- Strict mode is not optional for production codebases — it's the minimum viable safety net
Frequently Asked Questions
Yes — every error strict mode surfaces is a real bug. Migrate incrementally with `// @ts-strict-ignore` on files you haven't touched yet.
