Introduction
TypeScript has evolved significantly since its introduction. Modern TypeScript offers powerful features that, when used effectively, can dramatically improve code quality and developer experience.
This article explores advanced patterns we use in production applications.
Discriminated Unions
Discriminated unions are one of TypeScript's most powerful features for modeling complex state.
The Pattern
A discriminated union uses a common property (the discriminant) to distinguish between different variants of a type:
type LoadingState = { status: 'loading' }
type SuccessState = { status: 'success'; data: User[] }
type ErrorState = { status: 'error'; error: Error }
type State = LoadingState | SuccessState | ErrorState
Benefits
This pattern enables exhaustive checking, ensuring you handle all possible states:
function render(state: State) {
switch (state.status) {
case 'loading':
return <Spinner />
case 'success':
return <UserList users={state.data} />
case 'error':
return <ErrorMessage error={state.error} />
}
}
Branded Types
Branded types add nominal typing to TypeScript's structural type system.
The Problem
Consider two string types that are semantically different:
type UserId = string
type PostId = string
function getPost(postId: PostId): Post { ... }
const userId: UserId = 'user_123'
getPost(userId) // No error! But this is wrong.
The Solution
Branded types prevent this mixing:
type UserId = string & { readonly brand: unique symbol }
type PostId = string & { readonly brand: unique symbol }
const userId = 'user_123' as UserId
getPost(userId) // Error!
Type Predicates
Type predicates enable custom type guards that narrow types based on runtime checks.
Basic Usage
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'email' in value
)
}
Integration with Arrays
Type predicates work beautifully with array methods:
const items: (User | null)[] = [...]
const users: User[] = items.filter((item): item is User => item !== null)
Template Literal Types
Template literal types enable string manipulation at the type level.
API Route Types
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Route = '/users' | '/posts' | '/comments'
type Endpoint = ${Method} ${Route}
// Endpoint = 'GET /users' | 'GET /posts' | ...
Conditional Types
Conditional types enable complex type transformations.
Extracting Types
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T
type Result = Awaited<Promise<Promise<string>>> // string
Conclusion
These patterns represent just a fraction of what modern TypeScript offers. The key is to choose patterns that improve clarity and safety without adding unnecessary complexity.
Start by identifying the specific problems you're facing—type safety issues, unclear state modeling, or mixing of semantically different values—and apply the appropriate pattern to address them.



