TypeScript Beyond the Basics
Most TypeScript tutorials stop at interfaces and generics. But the real power of TypeScript lies in patterns that eliminate entire categories of bugs at compile time.
Discriminated Unions
The most useful pattern for handling different states:
typescript
type ApiResult<T> =
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
function handleResult<T>(result: ApiResult<T>) {
switch (result.status) {
case 'loading':
return <Spinner />
case 'success':
return <DataView data={result.data} />
case 'error':
return <ErrorView error={result.error} />
}
}TypeScript narrows the type in each branch automatically.
Branded Types
Prevent mixing up values that share the same primitive type:
typescript
type UserId = string & { readonly __brand: 'UserId' }
type OrderId = string & { readonly __brand: 'OrderId' }
function getUser(id: UserId): Promise<User> { /* ... */ }
function getOrder(id: OrderId): Promise<Order> { /* ... */ }
// Compiler error: can't pass OrderId where UserId expected
getUser(orderId) // Type error!Type-Safe Error Handling
Replace try-catch with explicit error types:
typescript
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E }
async function parseConfig(path: string): Promise<Result<Config, ParseError>> {
try {
const raw = await readFile(path)
const config = JSON.parse(raw)
return { ok: true, value: config }
} catch (e) {
return { ok: false, error: new ParseError(e.message) }
}
}These patterns aren't academic exercises. They're practical tools that make your codebase more reliable, self-documenting, and easier to refactor. The type system is your most powerful testing tool — use it.