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.