The TypeScript Patterns I Use Every Day
After years of TypeScript in production, a few patterns show up in every codebase I build. These are the ones that consistently reduce bugs and improve maintainability.
TypeScript is not just about adding types to JavaScript. Used well, it shapes how you design code. Here are the patterns I reach for most often.
1. Discriminated unions for state
Instead of multiple optional booleans (isLoading, isError, isSuccess), use a discriminated union. The compiler will force you to handle every case.
type FetchState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'error'; error: string }
| { status: 'success'; data: T };
// TypeScript narrows automatically:
if (state.status === 'success') {
console.log(state.data); // ✅ TypeScript knows data exists
}2. Branded types for IDs
Passing a userId where a postId is expected is a runtime bug TypeScript misses by default. Branded types fix this with zero runtime cost.
type UserId = string & { readonly __brand: 'UserId' };
type PostId = string & { readonly __brand: 'PostId' };
function createUserId(id: string): UserId {
return id as UserId;
}
// Now TypeScript catches misuse at compile time:
function getUser(id: UserId) { ... }
getUser(postId); // ❌ Error — expected UserId, got PostId3. Satisfies instead of as
The 'satisfies' keyword validates that a value matches a type without widening it. Use it for config objects where you want autocomplete but also need the literal types.
const routes = {
home: '/',
about: '/about',
blog: '/blog',
} satisfies Record<string, string>;
// routes.home is still type '/' (not string)
// Great for route maps, config objects, etc.4. Const assertion for lookup tables
const STATUS = {
ACTIVE: 'active',
INACTIVE: 'inactive',
PENDING: 'pending',
} as const;
type Status = typeof STATUS[keyof typeof STATUS];
// Type is 'active' | 'inactive' | 'pending'5. Infer for extracting types
When working with third-party libraries that do not export their types, infer lets you extract what you need.
type Awaited<T> = T extends Promise<infer U> ? U : T;
type RouteParams<T extends string> =
T extends `${string}[${infer Param}]${string}`
? { [K in Param]: string }
: never;TypeScript's value comes from constraining the space of valid programs. These patterns are not clever tricks — they are ways of making illegal states unrepresentable.