TypeScript 5.0+ Advanced Patterns: Template Literals, Decorators, and More
TypeScript 5.0+ has introduced powerful features that enable more expressive and type-safe code. After working extensively with these new capabilities, I want to share the most impactful patterns and techniques that will elevate your TypeScript game.
Template Literal Types Revolution
Template literal types have transformed how we handle string manipulation at the type level:
API Route Type Safety
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type ApiVersion = 'v1' | 'v2'
type Resource = 'users' | 'posts' | 'comments'
type ApiRoute<
Method extends HttpMethod = HttpMethod,
Version extends ApiVersion = ApiVersion,
Path extends Resource = Resource
> = `${Method} /api/${Version}/${Path}`
// Usage
type UserRoutes = ApiRoute<'GET' | 'POST', 'v1', 'users'>
// Result: "GET /api/v1/users" | "POST /api/v1/users"
const handleRequest = (route: ApiRoute) => {
// Fully type-safe routing
}
CSS-in-JS Type Safety
type CSSUnit = 'px' | 'rem' | 'em' | '%' | 'vh' | 'vw'
type CSSValue<T extends string> = `${number}${T}`
type Spacing = CSSValue<'px' | 'rem'>
type Percentage = CSSValue<'%'>
interface StyledProps {
margin?: Spacing
padding?: Spacing
width?: Spacing | Percentage
height?: Spacing | Percentage
}
// Usage with full autocomplete and validation
const Button = styled.button<StyledProps>`
margin: ${props => props.margin}; // Type-safe: only accepts "10px", "1rem", etc.
padding: ${props => props.padding};
`
Advanced Decorators
The new decorators proposal brings powerful metaprogramming capabilities:
Method Validation Decorator
function validate<T>(
validator: (value: T) => boolean,
errorMessage: string
) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value
descriptor.value = function (...args: any[]) {
const isValid = args.every(arg => validator(arg))
if (!isValid) {
throw new Error(`${propertyKey}: ${errorMessage}`)
}
return originalMethod.apply(this, args)
}
}
}
class UserService {
@validate<string>(val => val.length > 0, 'Name cannot be empty')
@validate<string>(val => val.includes('@'), 'Invalid email format')
createUser(name: string, email: string) {
// Method implementation
return { id: Date.now(), name, email }
}
}
Performance Monitoring Decorator
function measurePerformance(threshold: number = 100) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value
descriptor.value = async function (...args: any[]) {
const start = performance.now()
try {
const result = await originalMethod.apply(this, args)
const duration = performance.now() - start
if (duration > threshold) {
console.warn(`${propertyKey} took ${duration.toFixed(2)}ms (threshold: ${threshold}ms)`)
}
return result
} catch (error) {
const duration = performance.now() - start
console.error(`${propertyKey} failed after ${duration.toFixed(2)}ms:`, error)
throw error
}
}
}
}
class DataService {
@measurePerformance(500)
async fetchLargeDataset() {
// Expensive operation
}
}
Const Assertions and Immutable Patterns
Const assertions enable powerful immutable patterns:
Configuration Objects
const config = {
api: {
baseUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
},
features: {
darkMode: true,
analytics: false,
experiments: ['feature-a', 'feature-b']
}
} as const
type Config = typeof config
type ApiConfig = Config['api']
type FeatureFlags = Config['features']
type ExperimentName = Config['features']['experiments'][number]
// Result: "feature-a" | "feature-b"
// Type-safe configuration access
function getFeatureFlag<K extends keyof FeatureFlags>(
flag: K
): FeatureFlags[K] {
return config.features[flag]
}
State Machine Types
const states = ['idle', 'loading', 'success', 'error'] as const
const events = ['FETCH', 'SUCCESS', 'ERROR', 'RESET'] as const
type State = typeof states[number]
type Event = typeof events[number]
type StateMachine = {
[S in State]: {
[E in Event]?: State
}
}
const machine: StateMachine = {
idle: { FETCH: 'loading' },
loading: { SUCCESS: 'success', ERROR: 'error' },
success: { FETCH: 'loading', RESET: 'idle' },
error: { FETCH: 'loading', RESET: 'idle' }
}
function transition(currentState: State, event: Event): State {
return machine[currentState][event] ?? currentState
}
Advanced Generic Patterns
Conditional Types for API Responses
type ApiResponse<T> = T extends { error: any }
? { success: false; error: T['error'] }
: { success: true; data: T }
type UserData = { id: number; name: string }
type ErrorData = { error: string }
type UserResponse = ApiResponse<UserData>
// Result: { success: true; data: { id: number; name: string } }
type ErrorResponse = ApiResponse<ErrorData>
// Result: { success: false; error: string }
async function handleApiCall<T>(
promise: Promise<T>
): Promise<ApiResponse<T>> {
try {
const data = await promise
return { success: true, data } as ApiResponse<T>
} catch (error) {
return { success: false, error } as ApiResponse<T>
}
}
Mapped Types for Form Validation
type ValidationRule<T> = {
required?: boolean
minLength?: T extends string ? number : never
maxLength?: T extends string ? number : never
min?: T extends number ? number : never
max?: T extends number ? number : never
pattern?: T extends string ? RegExp : never
}
type FormValidation<T> = {
[K in keyof T]: ValidationRule<T[K]>
}
interface UserForm {
name: string
email: string
age: number
bio?: string
}
const userValidation: FormValidation<UserForm> = {
name: { required: true, minLength: 2, maxLength: 50 },
email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
age: { required: true, min: 18, max: 120 },
bio: { maxLength: 500 }
}
Utility Types and Helpers
Deep Readonly and Mutable
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P]
}
type DeepMutable<T> = {
-readonly [P in keyof T]: T[P] extends object
? DeepMutable<T[P]>
: T[P]
}
interface NestedConfig {
database: {
host: string
port: number
credentials: {
username: string
password: string
}
}
}
type ReadonlyConfig = DeepReadonly<NestedConfig>
// All properties and nested properties become readonly
type MutableConfig = DeepMutable<ReadonlyConfig>
// Removes all readonly modifiers
Path-based Property Access
type PathImpl<T, Key extends keyof T> = Key extends string
? T[Key] extends Record<string, any>
?
| `${Key}.${PathImpl<T[Key], Exclude<keyof T[Key], keyof any[]>> & string}`
| `${Key}.${Exclude<keyof T[Key], keyof any[]> & string}`
: never
: never
type Path<T> = PathImpl<T, keyof T> | keyof T
type PathValue<T, P extends Path<T>> = P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? Rest extends Path<T[Key]>
? PathValue<T[Key], Rest>
: never
: never
: P extends keyof T
? T[P]
: never
function get<T, P extends Path<T>>(obj: T, path: P): PathValue<T, P> {
return path.split('.').reduce((current, key) => current?.[key], obj) as PathValue<T, P>
}
// Usage
const data = {
user: {
profile: {
name: 'John',
settings: {
theme: 'dark'
}
}
}
}
const theme = get(data, 'user.profile.settings.theme') // Type: string
Real-World Application Patterns
Type-Safe Event System
interface EventMap {
'user:login': { userId: string; timestamp: Date }
'user:logout': { userId: string }
'order:created': { orderId: string; amount: number }
'order:cancelled': { orderId: string; reason: string }
}
class TypedEventEmitter<T extends Record<string, any>> {
private listeners: {
[K in keyof T]?: Array<(data: T[K]) => void>
} = {}
on<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = []
}
this.listeners[event]!.push(listener)
}
emit<K extends keyof T>(event: K, data: T[K]): void {
const eventListeners = this.listeners[event]
if (eventListeners) {
eventListeners.forEach(listener => listener(data))
}
}
}
const eventBus = new TypedEventEmitter<EventMap>()
// Type-safe event handling
eventBus.on('user:login', (data) => {
// data is typed as { userId: string; timestamp: Date }
console.log(`User ${data.userId} logged in at ${data.timestamp}`)
})
Database Query Builder
interface User {
id: number
name: string
email: string
createdAt: Date
}
type WhereClause<T> = {
[K in keyof T]?: T[K] | { $in: T[K][] } | { $gt: T[K] } | { $lt: T[K] }
}
type SelectClause<T> = {
[K in keyof T]?: boolean
}
class QueryBuilder<T> {
private table: string
private whereClause: WhereClause<T> = {}
private selectClause: SelectClause<T> = {}
constructor(table: string) {
this.table = table
}
where<K extends keyof T>(field: K, value: T[K] | { $in: T[K][] }): this {
this.whereClause[field] = value
return this
}
select<K extends keyof T>(...fields: K[]): QueryBuilder<Pick<T, K>> {
const newSelectClause = {} as SelectClause<T>
fields.forEach(field => {
newSelectClause[field] = true
})
return Object.assign(new QueryBuilder<Pick<T, K>>(this.table), {
whereClause: this.whereClause,
selectClause: newSelectClause
})
}
build(): string {
const selectedFields = Object.keys(this.selectClause).length > 0
? Object.keys(this.selectClause).join(', ')
: '*'
let query = `SELECT ${selectedFields} FROM ${this.table}`
if (Object.keys(this.whereClause).length > 0) {
const conditions = Object.entries(this.whereClause)
.map(([field, value]) => `${field} = ${JSON.stringify(value)}`)
.join(' AND ')
query += ` WHERE ${conditions}`
}
return query
}
}
// Usage with full type safety
const userQuery = new QueryBuilder<User>('users')
.where('email', 'john@example.com')
.select('id', 'name') // Result type: QueryBuilder<Pick<User, 'id' | 'name'>>
.build()
Performance and Best Practices
Optimizing Type Compilation
// Use interfaces for object shapes (faster compilation)
interface UserProps {
id: number
name: string
}
// Use type aliases for unions and computed types
type Status = 'pending' | 'approved' | 'rejected'
// Prefer const assertions over enums for better tree-shaking
const STATUSES = ['pending', 'approved', 'rejected'] as const
type Status = typeof STATUSES[number]
// Use generic constraints to improve inference
function processItems<T extends { id: string }>(items: T[]): T[] {
return items.filter(item => item.id.length > 0)
}
Conclusion
TypeScript 5.0+ has elevated the language to new heights of expressiveness and type safety. These advanced patterns enable you to build more robust applications while maintaining excellent developer experience.
The key is to gradually adopt these patterns where they provide clear value. Start with template literal types for string validation, explore decorators for cross-cutting concerns, and leverage const assertions for immutable data structures.
Remember: TypeScript's power lies not just in catching errors, but in enabling confident refactoring and providing excellent IDE support. These advanced patterns amplify those benefits while keeping your code maintainable and expressive.
