TypeScript Design Patterns

Design patterns are proven solutions to recurring problems in software design. They provide a structured approach to solving common issues and improving code maintainability, readability, and scalability. In TypeScript, design patterns can be implemented to harness the power of object-oriented programming (OOP) and help developers create cleaner, more efficient code.

Creational Design Patterns

Creational design patterns deal with the creation of objects in a manner that suits specific situations. They provide solutions to create object instances that are suitable to the context of the program.

Singleton Pattern

The Singleton pattern ensures that a class has only one instance, and it provides a global point of access to it. This is useful when exactly one object is needed to coordinate actions across a system.

Example:

				
					class Singleton {
    private static instance: Singleton;

    private constructor() {
        // Private constructor prevents instantiation from other classes
    }

    public static getInstance(): Singleton {
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }

    public showMessage(): void {
        console.log("Singleton instance method called!");
    }
}

const singleton1 = Singleton.getInstance();
const singleton2 = Singleton.getInstance();

singleton1.showMessage();  // Output: Singleton instance method called!

console.log(singleton1 === singleton2);  // Output: true (both are same instance)

				
			

Explanation:

  • private constructor prevents instantiation from outside the class.
  • getInstance() returns the single instance of the class.
  • The Singleton pattern ensures that there is only one instance of Singleton.

Output:

				
					Singleton instance method called!
true

				
			

Factory Pattern

The Factory pattern provides an interface for creating objects in a super class, but allows subclasses to alter the type of objects that will be created.

Example:

				
					interface Product {
    operation(): string;
}

class ConcreteProductA implements Product {
    public operation(): string {
        return "Result of ConcreteProductA";
    }
}

class ConcreteProductB implements Product {
    public operation(): string {
        return "Result of ConcreteProductB";
    }
}

class ProductFactory {
    public static createProduct(type: string): Product {
        if (type === "A") {
            return new ConcreteProductA();
        } else if (type === "B") {
            return new ConcreteProductB();
        } else {
            throw new Error("Unknown product type");
        }
    }
}

const productA = ProductFactory.createProduct("A");
console.log(productA.operation()); // Output: Result of ConcreteProductA

const productB = ProductFactory.createProduct("B");
console.log(productB.operation()); // Output: Result of ConcreteProductB

				
			

Explanation:

  • ProductFactory is used to create different product types.
  • createProduct() returns different instances based on the product type passed in.

Output:

				
					Result of ConcreteProductA
Result of ConcreteProductB
				
			

Builder Pattern

The Builder pattern allows for the step-by-step construction of complex objects. It separates the construction of a complex object from its representation, meaning the same construction process can create different representations.

Example:

				
					class Car {
    public engine: string;
    public seats: number;
    public color: string;

    constructor() {
        this.engine = "";
        this.seats = 0;
        this.color = "";
    }
}

class CarBuilder {
    private car: Car;

    constructor() {
        this.car = new Car();
    }

    setEngine(engine: string): CarBuilder {
        this.car.engine = engine;
        return this;
    }

    setSeats(seats: number): CarBuilder {
        this.car.seats = seats;
        return this;
    }

    setColor(color: string): CarBuilder {
        this.car.color = color;
        return this;
    }

    build(): Car {
        return this.car;
    }
}

const carBuilder = new CarBuilder();
const myCar = carBuilder.setEngine("V8").setSeats(4).setColor("Red").build();
console.log(myCar); // Output: { engine: 'V8', seats: 4, color: 'Red' }

				
			

Explanation:

  • The Builder pattern is used to construct complex objects (e.g., a Car) step by step.
  • The CarBuilder class allows us to chain method calls to build a car with specific configurations.

Output:

				
					{ engine: 'V8', seats: 4, color: 'Red' }
				
			

Structural Design Patterns

Structural design patterns are concerned with how classes and objects are composed to form larger structures. These patterns simplify the structure by identifying relationships between objects.

Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together. It is used to make one object compatible with another by providing a wrapper around it.

Example:

				
					interface Target {
    request(): string;
}

class Adaptee {
    public specificRequest(): string {
        return "Specific request from Adaptee";
    }
}

class Adapter implements Target {
    private adaptee: Adaptee;

    constructor(adaptee: Adaptee) {
        this.adaptee = adaptee;
    }

    public request(): string {
        return this.adaptee.specificRequest();
    }
}

const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
console.log(adapter.request()); // Output: Specific request from Adaptee

				
			

Explanation:

  • The Adapter pattern wraps an incompatible class (Adaptee) so it can be used by a client expecting a different interface (Target).

Output:

				
					Specific request from Adaptee
				
			

Facade Pattern

The Facade pattern provides a simplified interface to a complex system of classes, libraries, or frameworks.

Example:

				
					class SubsystemA {
    public operationA(): string {
        return "Subsystem A operation";
    }
}

class SubsystemB {
    public operationB(): string {
        return "Subsystem B operation";
    }
}

class Facade {
    private subsystemA: SubsystemA;
    private subsystemB: SubsystemB;

    constructor() {
        this.subsystemA = new SubsystemA();
        this.subsystemB = new SubsystemB();
    }

    public performOperation(): string {
        const resultA = this.subsystemA.operationA();
        const resultB = this.subsystemB.operationB();
        return `${resultA}, ${resultB}`;
    }
}

const facade = new Facade();
console.log(facade.performOperation()); // Output: Subsystem A operation, Subsystem B operation

				
			

Explanation:

  • The Facade class provides a simple interface to work with complex subsystems (SubsystemA and SubsystemB).

Output:

				
					Subsystem A operation, Subsystem B operation

				
			

Composite Pattern

The Composite pattern allows clients to treat individual objects and compositions of objects uniformly. It is especially useful for building tree structures like directories and files.

Example:

				
					interface Component {
    operation(): string;
}

class Leaf implements Component {
    public operation(): string {
        return "Leaf";
    }
}

class Composite implements Component {
    private children: Component[] = [];

    public add(component: Component): void {
        this.children.push(component);
    }

    public operation(): string {
        return this.children.map(child => child.operation()).join(", ");
    }
}

const leaf1 = new Leaf();
const leaf2 = new Leaf();
const composite = new Composite();
composite.add(leaf1);
composite.add(leaf2);

console.log(composite.operation()); // Output: Leaf, Leaf

				
			

Explanation:

  • The Composite pattern combines individual components (Leaf) and composite components (Composite).
  • A composite component can contain multiple children, and the operation on the composite processes all children.

Output:

				
					Leaf, Leaf
				
			

Behavioral Design Patterns

Behavioral design patterns deal with communication between objects. They simplify how objects interact and communicate with each other.

Observer Pattern

The Observer pattern defines a one-to-many relationship between objects, where one object (subject) changes its state, and all of its dependents (observers) are notified automatically.

Example:

				
					interface Observer {
    update(message: string): void;
}

class Subject {
    private observers: Observer[] = [];

    public addObserver(observer: Observer): void {
        this.observers.push(observer);
    }

    public removeObserver(observer: Observer): void {
        this.observers = this.observers.filter(obs => obs !== observer);
    }

    public notifyObservers(message: string): void {
        for (let observer of this.observers) {
            observer.update(message);
        }
    }
}

class ConcreteObserver implements Observer {
    constructor(private name: string) {}

    public update(message: string): void {
        console.log(`${this.name} received message: ${message}`);
    }
}

const subject = new Subject();
const observer1 = new ConcreteObserver("Observer1");
const observer2 = new ConcreteObserver("Observer2");

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.notifyObservers("Update available!"); // Output: Observer1 received message: Update available! Observer2 received message: Update available!

				
			

Explanation:

  • The Observer pattern allows Observer objects to be notified when the Subject state changes.
  • Each observer receives the updated state when the subject notifies them.

Output:

				
					Observer1 received message: Update available!
Observer2 received message: Update available!
				
			

We explored several essential design patterns in TypeScript, covering:Creational Patterns like Singleton, Factory, and Builder, which deal with object creation. Structural Patterns like Adapter, Facade, and Composite, which focus on the structure and relationships of objects. Behavioral Patterns like Observer, which manage communication between objects. Happy Coding!❤️

Table of Contents