Design Patterns in TypeScript: Advanced Level with Examples
\n\nDeveloping complex applications in TypeScript requires not only knowledge of syntax but also the ability to build flexible, scalable architecture. The GoF (Gang of Four) design patterns remain the gold standard, but at an advanced level, their application becomes more nuanced. In this article, we will analyze four powerful patterns — Bridge, Visitor, State, and Strategy — with real-world examples in TypeScript. You will learn how to use strong typing, generics, and interfaces to implement these patterns.
\n\n1. Bridge: Separating Abstraction from Implementation
\n\nThe Bridge pattern separates an abstraction from its implementation, allowing them to vary independently. This is especially useful when you have multiple variations of functionality that can be combined.
\n\nExample: A shape rendering system using different graphics APIs (Canvas and SVG).
\n\n// Implementation\ninterface Renderer {\n renderCircle(radius: number): void;\n renderSquare(side: number): void;\n}\n\nclass CanvasRenderer implements Renderer {\n renderCircle(radius: number): void {\n console.log(`Canvas: drawing a circle with radius ${radius}`);\n }\n renderSquare(side: number): void {\n console.log(`Canvas: drawing a square with side ${side}`);\n }\n}\n\nclass SVGRenderer implements Renderer {\n renderCircle(radius: number): void {\n console.log(`SVG: <circle r="${radius}" />`);\n }\n renderSquare(side: number): void {\n console.log(`SVG: <rect width="${side}" height="${side}" />`);\n }\n}\n\n// Abstraction\nabstract class Shape {\n protected renderer: Renderer;\n\n constructor(renderer: Renderer) {\n this.renderer = renderer;\n }\n\n abstract draw(): void;\n}\n\nclass Circle extends Shape {\n private radius: number;\n\n constructor(renderer: Renderer, radius: number) {\n super(renderer);\n this.radius = radius;\n }\n\n draw(): void {\n this.renderer.renderCircle(this.radius);\n }\n}\n\nclass Square extends Shape {\n private side: number;\n\n constructor(renderer: Renderer, side: number) {\n super(renderer);\n this.side = side;\n }\n\n draw(): void {\n this.renderer.renderSquare(this.side);\n }\n}\n\n// Usage\nconst canvasCircle = new Circle(new CanvasRenderer(), 10);\ncanvasCircle.draw(); // Canvas: drawing a circle with radius 10\n\nconst svgSquare = new Square(new SVGRenderer(), 5);\nsvgSquare.draw(); // SVG: <rect width="5" height="5" />\n\nKey Feature: TypeScript guarantees that all implementations follow the Renderer contract. Adding a new shape type or a new renderer does not break existing code.
2. Visitor: Traversing Complex Structures Without Changing Classes
\n\nThe Visitor pattern allows adding new operations to objects without changing their classes. It is an ideal solution for working with trees, ASTs (Abstract Syntax Trees), or component hierarchies.
\n\nExample: A system for exporting document nodes to different formats (JSON, XML).
\n\n// Elements\ninterface DocumentNode {\n accept(visitor: Visitor): void;\n}\n\nclass TextNode implements DocumentNode {\n constructor(public content: string) {}\n\n accept(visitor: Visitor): void {\n visitor.visitTextNode(this);\n }\n}\n\nclass ImageNode implements DocumentNode {\n constructor(public src: string, public alt: string) {}\n\n accept(visitor: Visitor): void {\n visitor.visitImageNode(this);\n }\n}\n\n// Visitor\ninterface Visitor {\n visitTextNode(node: TextNode): void;\n visitImageNode(node: ImageNode): void;\n}\n\nclass JSONExporter implements Visitor {\n private p