Back to blogTechnology

TypeScript Patterns I Wish I Had Known Earlier

After five years writing TypeScript daily, these are the patterns that made the biggest difference to code quality, developer experience, and maintenance burden.

Flaka KallabaJune 19, 2026393 views
TypeScript Patterns I Wish I Had Known Earlier

Discriminated Unions Over Boolean Flags

Boolean flags multiply. A component that starts with isLoading gains isError, then isEmpty, then isRefetching. Before long you have illegal state: isLoading && isError is simultaneously true.

Model state explicitly:
type RequestState<T> =\n | { status: 'idle' }\n | { status: 'loading' }\n | { status: 'success'; data: T }\n | { status: 'error'; error: string }

Now the type system enforces valid combinations. You cannot access data without first narrowing to the success case.

const Assertions for Literal Types

When you need a type that is exactly a set of string literals, reach for as const:

const ROLES = ['admin', 'editor', 'viewer'] as const\ntype Role = typeof ROLES[number] // 'admin' | 'editor' | 'viewer'

This gives you a single source of truth that works at runtime (for validation) and compile time (for type checking).

Generic Constraints That Communicate Intent

Constraints are documentation. function pick<T, K extends keyof T> tells the reader immediately that K must be a key of T. Use them aggressively.

Utility Types Are Not Just for Library Authors

Every TypeScript project should be using Partial, Required, Pick, Omit, Record, and Readonly routinely. They let you derive types from existing types rather than duplicating them, which means changes propagate automatically.

If you find yourself writing the same shape twice, there is almost certainly a utility type that can derive one from the other.

Branded Types for Primitives That Are Not Interchangeable

User IDs and order IDs are both numbers, but they are not the same thing. A branded type catches swaps at compile time:

type UserId = number & { readonly __brand: 'UserId' }\ntype OrderId = number & { readonly __brand: 'OrderId' }\n\nfunction getUser(id: UserId): User { ... }

Now getUser(orderId) is a type error. The runtime cost is zero; the safety gain is real.

F

Flaka Kallaba

Author

More articles