TypeScript has evolved dramatically in recent versions. Features that once required elaborate workarounds are now built into the language. Yet many developers continue using TypeScript in ways that don't leverage these advances.
I'm not talking about basic generics or utility types—those are table stakes. I'm talking about advanced patterns that transform how you think about type safety.
Template Literal Types for Type-Safe APIs
Template literal types allow you to create types from string patterns. This is transformative for API route handling:
type ApiRoute =
| `/api/users/${string}`
| `/api/users/${string}/posts`
| `/api/posts/${string}`
async function apiFetch<T>(route: ApiRoute): Promise<T> {
return fetch(route).then(r => r.json())
}
apiFetch('/api/users/123') // ✅
apiFetch('/api/user/123') // ❌ Typo caught!
apiFetch('/users/123') // ❌ Missing /apiExtracting Parameters from Routes
type ExtractParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? { [K in Param]: string } & ExtractParams<Rest>
: T extends `${infer _Start}:${infer Param}`
? { [K in Param]: string }
: {}
type UserPostsParams = ExtractParams<'/users/:userId/posts/:postId'>
// Result: { userId: string; postId: string }Const Type Parameters
TypeScript 5.0 introduced const type parameters, solving a long-standing pain point—preserving literal types through function calls:
// Without const, literals widen
function createConfig<T>(config: T): T { return config }
const config = createConfig({ env: 'production', port: 3000 })
// config.env is string, not 'production' ❌
// With const type parameter
function createConfig<const T>(config: T): T { return config }
const config = createConfig({ env: 'production', port: 3000 })
// config.env is 'production' ✅The Satisfies Operator
The satisfies operator bridges the gap between type annotations and type inference:
// Type annotation - loses specificity
const routes: Record<string, { path: string; auth: boolean }> = {
home: { path: '/', auth: false },
dashboard: { path: '/dashboard', auth: true },
}
routes.typo // No error! ❌
// satisfies - validates AND preserves
const routes = {
home: { path: '/', auth: false },
dashboard: { path: '/dashboard', auth: true },
} satisfies Record<string, { path: string; auth: boolean }>
routes.home.path // '/' (literal preserved!) ✅
routes.typo // ❌ Error: property doesn't existCombining Conditional and Mapped Types
type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
interface User {
id: string; name: string; email: string; avatar: string
}
type CreateUserInput = MakeOptional<User, 'id' | 'avatar'>
// { name: string; email: string; id?: string; avatar?: string }Building Type-Safe Event Emitters
type EventMap = Record<string, unknown[]>
class TypedEventEmitter<T extends EventMap> {
private listeners: { [K in keyof T]?: Set<(...args: T[K]) => void> } = {}
on<K extends keyof T>(event: K, listener: (...args: T[K]) => void): () => void {
if (!this.listeners[event]) this.listeners[event] = new Set()
this.listeners[event]!.add(listener)
return () => this.listeners[event]?.delete(listener)
}
emit<K extends keyof T>(event: K, ...args: T[K]): void {
this.listeners[event]?.forEach(l => l(...args))
}
}
interface AppEvents {
'user:login': [userId: string, timestamp: Date]
'error': [error: Error, context: string]
}
const emitter = new TypedEventEmitter<AppEvents>()
emitter.emit('user:login', 'user-123', new Date()) // ✅
emitter.emit('user:login', 'user-123') // ❌ Missing timestampConclusion
Advanced TypeScript isn't about showing off—it's about catching bugs at compile time that would otherwise reach production. The investment in learning these patterns pays dividends every day: fewer runtime errors, better IDE support, and code that documents itself.



