TypeScript Patterns I Wish I Knew Earlier
Practical tips that make your code safer and cleaner
TypeScript's type system is deep. Most developers (including past me) use 20% of it for 80% of the benefit. Here are the other 20% of features that are actually worth learning.
Discriminated Unions for State Machines
Instead of optional fields that may or may not exist, model state explicitly:
// Bad: optional fields create ambiguous state
type State = {
loading: boolean;
data?: User;
error?: string;
}
// Good: each state is unambiguous
type State =
| { status: 'loading' }
| { status: 'success'; data: User }
| { status: 'error'; error: string }
TypeScript narrows types automatically in each branch. No more data?.thing ?? undefined gymnastics throughout your component.
The satisfies Operator
Added in TS 4.9, satisfies validates that a value matches a type without widening the inferred type. Great for configuration objects where you want both type safety and precise autocomplete on the specific values.
Branded Types
Prevent accidentally mixing values of the same primitive type:
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };
// TypeScript catches you passing an OrderId where UserId is expected
function getUser(id: UserId) { ... }
Template Literal Types
Build string patterns at the type level — great for event names, API routes, or CSS class combinations. These let you express constraints that would otherwise only be caught at runtime.
TypeScript's value is in making implicit contracts explicit. The more you embrace that philosophy, the fewer runtime bugs you ship.