# Advanced TypeScript Patterns: From Good to Great
TypeScript has become the standard for modern JavaScript development. But there's a huge difference between basic TypeScript and advanced, type-safe code that catches bugs before they happen. Let me share the patterns that separate good TypeScript code from great.
## Why Advanced TypeScript Matters
Basic TypeScript gives you autocomplete and catches obvious bugs. Advanced TypeScript:
- Makes invalid states unrepresentable
- Catches logic errors at compile time
- Provides self-documenting code
- Enables fearless refactoring
- Improves developer experience
## Pattern 1: Discriminated Unions
Model your domain with discriminated unions to make impossible states impossible.
### Before:
```typescript
interface State {
loading: boolean
error: Error | null
data: User | null
}
// Problem: data could be null while loading is false
```
### After:
```typescript
type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'error'; error: Error }
| { status: 'success'; data: User }
// Now TypeScript knows exactly what properties exist
```
## Pattern 2: Branded Types
Create nominal types for type safety:
```typescript
type UserId = string & { readonly brand: unique symbol }
type ProductId = string & { readonly brand: unique symbol }
function getUser(id: UserId) { }
function getProduct(id: ProductId) { }
const userId = 'user-123' as UserId
const productId = 'product-456' as ProductId
getUser(productId) // Error! 🎉
```
## Pattern 3: Builder Pattern with Fluent API
```typescript
class QueryBuilder<T> {
private filters: Filter[] = []
where(condition: Filter): QueryBuilder<T> {
this.filters.push(condition)
return this
}
select<K extends keyof T>(...fields: K[]): QueryBuilder<Pick<T, K>> {
return this as any
}
async execute(): Promise<T[]> {
// Execute query
}
}
// Usage with full type safety
const users = await new QueryBuilder<User>()
.where({ age: { gt: 18 } })
.select('name', 'email')
.execute()
// users is User[] with only name and email properties
```
## Pattern 4: Template Literal Types
```typescript
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Route = '/users' | '/products' | '/orders'
type API = `${HTTPMethod} ${Route}`
// Valid: 'GET /users', 'POST /products', etc.
// Invalid: 'PATCH /users', 'GET /invalid'
function callAPI(endpoint: API) { }
callAPI('GET /users') // ✅
callAPI('PATCH /users') // ❌ Error!
```
## Pattern 5: Recursive Types
```typescript
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue }
const validJSON: JSONValue = {
name: 'John',
age: 30,
hobbies: ['reading', 'coding'],
address: {
city: 'NYC',
coordinates: [40.7128, -74.0060],
},
}
```
## Pattern 6: Conditional Types
```typescript
type IsString<T> = T extends string ? true : false
type A = IsString<string> // true
type B = IsString<number> // false
// More practical example
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T]
interface User {
name: string
age: number
save: () => void
delete: () => void
}
type UserMethods = FunctionPropertyNames<User>
// 'save' | 'delete'
```
## Pattern 7: Mapped Types with Key Remapping
```typescript
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
type Person = {
name: string
age: number
}
type PersonGetters = Getters<Person>
// {
// getName: () => string
// getAge: () => number
// }
```
## Pattern 8: Utility Type Combinations
```typescript
// Make specific properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
interface User {
id: string
name: string
email: string
age: number
}
type UserUpdate = PartialBy<User, 'name' | 'age'>
// id and email required, name and age optional
// Deep readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K]
}
```
## Pattern 9: Type Guards
```typescript
interface Dog {
type: 'dog'
bark: () => void
}
interface Cat {
type: 'cat'
meow: () => void
}
type Animal = Dog | Cat
function isDog(animal: Animal): animal is Dog {
return animal.type === 'dog'
}
function handleAnimal(animal: Animal) {
if (isDog(animal)) {
animal.bark() // TypeScript knows it's a Dog
} else {
animal.meow() // TypeScript knows it's a Cat
}
}
```
## Pattern 10: Generic Constraints
```typescript
interface Identifiable {
id: string
}
function findById<T extends Identifiable>(
items: T[],
id: string
): T | undefined {
return items.find(item => item.id === id)
}
// Works with any type that has an id
const user = findById(users, 'user-123')
const product = findById(products, 'product-456')
```
## Real-World Example: Type-Safe API Client
```typescript
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
interface APIEndpoint {
method: HTTPMethod
path: string
body?: unknown
response: unknown
}
type API = {
'GET /users': {
method: 'GET'
path: '/users'
response: User[]
}
'GET /users/:id': {
method: 'GET'
path: '/users/:id'
response: User
}
'POST /users': {
method: 'POST'
path: '/users'
body: CreateUserDTO
response: User
}
}
type ExtractParams<T extends string> =
T extends `${infer Start}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractParams<Rest>]: string }
: T extends `${infer Start}:${infer Param}`
? { [K in Param]: string }
: {}
async function api<K extends keyof API>(
endpoint: K,
options: {
params?: ExtractParams<API[K]['path']>
body?: API[K] extends { body: infer B } ? B : never
} = {}
): Promise<API[K]['response']> {
// Implementation
}
// Fully type-safe usage
const users = await api('GET /users') // User[]
const user = await api('GET /users/:id', {
params: { id: '123' }
}) // User
const newUser = await api('POST /users', {
body: { name: 'John', email: 'john@example.com' }
}) // User
```
## Best Practices
### 1. Prefer Type Inference
```typescript
// Bad
const numbers: number[] = [1, 2, 3]
// Good
const numbers = [1, 2, 3] // TypeScript infers number[]
```
### 2. Use `const` Assertions
```typescript
const config = {
api: 'https://api.example.com',
timeout: 5000,
} as const
// config.api is 'https://api.example.com', not string
```
### 3. Avoid Type Assertions
```typescript
// Bad
const user = data as User
// Good
function isUser(data: unknown): data is User {
return typeof data === 'object' && data !== null && 'id' in data
}
if (isUser(data)) {
// data is User
}
```
### 4. Use Unknown Over Any
```typescript
// Bad
function process(data: any) {
data.whatever() // No type checking
}
// Good
function process(data: unknown) {
if (typeof data === 'object' && data !== null) {
// Now we can safely work with data
}
}
```
## Performance Considerations
1. **Avoid Complex Unions**: Union types with 1000+ members slow down compilation
2. **Use Module Augmentation Sparingly**: Can impact compilation performance
3. **Prefer Interfaces Over Types**: For object shapes, interfaces perform better
4. **Use Project References**: For large monorepos
## Conclusion
Advanced TypeScript patterns transform your code from "has types" to "impossible to misuse." These patterns catch bugs at compile time, provide better IDE support, and make your code self-documenting.
Start incorporating these patterns gradually. Your future self (and teammates) will thank you.
Remember: The goal isn't to use every advanced feature, but to write code that's impossible to use incorrectly.
Happy typing! 🎯
