TYPESCRIPT PATTERNS FOR LARGE CODEBASES
[2024.01.22] 08_MIN_READ
Introduction
TypeScript’s type system is expressive enough to encode most business logic constraints at compile time. This article covers patterns that pay dividends at scale.
Branded Types
Prevent mixing semantically different primitives:
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };
function brandAs<T>(value: string): T {
return value as unknown as T;
}
const userId = brandAs<UserId>('usr_123');
const orderId = brandAs<OrderId>('ord_456');
// TypeScript error: Argument of type 'OrderId' is not assignable to parameter of type 'UserId'
processUser(orderId);
Discriminated Unions for State Machines
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
Template Literal Types
type EventName = `on${Capitalize<string>}`;
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiRoute = `/${string}`;
type ApiEndpoint = `${HttpMethod} ${ApiRoute}`;
Conclusion
These patterns add compile-time safety with minimal runtime overhead. The investment in types pays off when refactoring large codebases.