Software Engineering7 min read

Design Patterns in TypeScript

Implement GoF design patterns in TypeScript: Factory, Observer, Strategy, Decorator, and Command - when to use them and when they add unnecessary complexity.

design-patternstypescriptarchitectureoop

Design patterns are reusable solutions to commonly occurring problems in software design. TypeScript's type system enables patterns with strong type safety. The key is recognizing when a pattern solves a real problem - not applying patterns for their own sake.

Reach for patterns when you recognize the problem they solve - not before. The most useful patterns in modern TypeScript: Strategy (swap algorithms), Observer (event systems), Factory (decouple creation from use), Decorator (cross-cutting concerns). Skip heavy GoF patterns (Visitor, Chain of Responsibility) unless you genuinely need them.

Workflow

  1. Factory Method and Abstract FactoryFactory patterns decouple the code that uses an object from the code that creates it.
  2. Observer pattern for event systemsObserver decouples event producers from consumers - add/remove listeners without modifying the producer.
  3. Strategy pattern for swappable algorithmsStrategy lets you select an algorithm at runtime, swap implementations without changing calling code.
  4. Decorator pattern for cross-cutting concernsDecorators add behavior to objects without modifying them - logging, caching, auth checks.
  5. Command pattern for undo/redoCommand encapsulates actions as objects, enabling undo/redo, queuing, and logging of operations.

Output Format

Factory function or class, typed EventEmitter with EventMap, Strategy interface with injectable implementations, function-based Decorator wrappers, Command with execute/undo and history stack.

Examples

Typed observer: `const emitter = new TypedEventEmitter<{ "data:loaded": Data[]; "error": Error }>(); emitter.on("data:loaded", (data) => setItems(data)); emitter.on("error", (err) => setError(err.message)); emitter.emit("data:loaded", items) // TypeScript knows items is Data[]`
Strategy as function: `type PricingStrategy = (basePrice: number, user: User) => number; const regularPricing: PricingStrategy = (p) => p; const premiumPricing: PricingStrategy = (p) => p * 0.8; const enterprisePricing: PricingStrategy = (p) => p * 0.6; const getPrice = (strategy: PricingStrategy) => strategy(product.basePrice, user)`
Composable decorators: `const fetchUser = async (id: string) => { /* API call */ }; const cachedFetchUser = withCache(fetchUser, { ttl: 60000 }); const loggedCachedFetchUser = withLogging(cachedFetchUser, "fetchUser"); const fetchUserWithRetry = withRetry(loggedCachedFetchUser, { maxAttempts: 3 })`

Edge Cases & Gotchas

  • Singleton: the most over-used pattern. Module singletons (import a module-level const) are the correct JavaScript singleton - avoid Singleton classes which are hard to test
  • Decorator vs TypeScript decorators: the GoF Decorator pattern (object wrapping) is different from TypeScript/JavaScript decorators (metadata annotations). Both are valid but solve different problems
  • Pattern recognition: patterns are recognizable by the problem, not the implementation. A `switch` statement may be the right solution; not every conditional needs a Strategy pattern

When to Break These Rules

  • Simple procedural code (scripts, data transforms, glue code) is cleaner without patterns. A flat sequence of function calls is often more readable than a class hierarchy implementing 3 design patterns.

Reference Files

  • Refactoring.Guru patterns: https://refactoring.guru/design-patterns
  • TypeScript patterns: https://www.patterns.dev/
  • Functional design patterns: https://mostly-adequate.gitbook.io/
  • Gang of Four (original): Design Patterns - Elements of Reusable OO Software, 1994

More skills in this library

Browse all technical skills - Mobile, AI/ML, Web, Backend, Design, and Product.