When Interfaces Are Not Enough
TypeScript's structural typing is powerful but can erode precision: widening literals, losing autocomplete on const objects, and allowing invalid states that compile silently. Modern TypeScript (4.9+) adds tools that preserve developer ergonomics while tightening correctness.
The satisfies Operator
satisfies validates that an expression matches a type without widening it to that type. You keep literal inference and still catch missing or extra keys.
type Route = Record<string, { path: string; method: 'GET' | 'POST' }>;
const routes = {
home: { path: '/', method: 'GET' },
login: { path: '/login', method: 'POST' },
} satisfies Route;
// routes.home.method is 'GET' | 'POST', not string
// Typo in key or method is caught at compile time
Discriminated Unions for State Machines
Model UI and API states with a shared discriminant field (kind, status, type). Control flow narrowing eliminates impossible branches without runtime checks scattered everywhere.
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function render<T>(state: AsyncState<T>) {
switch (state.status) {
case 'success': return state.data; // narrowed
case 'error': return state.error.message;
default: return null;
}
}
Branded Types for Domain Safety
Prevent mixing primitive IDs at compile time by branding strings:
type UserId = string & { readonly brand: unique symbol };
type OrderId = string & { readonly brand: unique symbol };
function getUser(id: UserId) { /* ... */ }
// getUser(orderId) // compile error
Template Literal Types
Generate CSS property unions, event names, or API route strings from const tuples. Pair with as const for design-system tokens and typed environment variable maps.
Advanced TypeScript is not academic. It reduces production bugs, improves IDE navigation, and documents domain rules in code. Invest in types at boundaries: API payloads, config objects, and shared state shapes.