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 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.
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.
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)
private constructor
prevents instantiation from outside the class.getInstance()
returns the single instance of the class.Singleton
.
Singleton instance method called!
true
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.
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
ProductFactory
is used to create different product types.createProduct()
returns different instances based on the product type passed in.
Result of ConcreteProductA
Result of ConcreteProductB
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.
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' }
Car
) step by step.
{ engine: 'V8', seats: 4, color: 'Red' }
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.
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.
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
Adaptee
) so it can be used by a client expecting a different interface (Target
).
Specific request from Adaptee
The Facade pattern provides a simplified interface to a complex system of classes, libraries, or frameworks.
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
SubsystemA
and SubsystemB
).
Subsystem A operation, Subsystem B operation
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.
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
Leaf
) and composite components (Composite
).
Leaf, Leaf
Behavioral design patterns deal with communication between objects. They simplify how objects interact and communicate with each other.
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.
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!
Observer
objects to be notified when the Subject
state changes.
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!❤️