design-patterns-in-typescript
Design Patterns in TypeScript:
These patterns are key for structuring TypeScript applications effectively:
Singleton Pattern:
The Singleton pattern ensures a class has only one instance and provides a single point of access to it.
Example:
class DatabaseConnection {
private static instance: DatabaseConnection;
private constructor() {
// Initialize connection
}
public static getInstance(): DatabaseConnection {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection();
}
return DatabaseConnection.instance;
}
}
Factory Pattern:
The Factory pattern provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created.
Example:
interface Product {
use(): void;
}
class ConcreteProductA implements Product {
public use(): void {
// Implementation for Product A
}
}
class ConcreteProductB implements Product {
public use(): void {
// Implementation for Product B
}
}
class ProductFactory {
public static createProduct(type: 'A' | 'B'): Product {
if (type === 'A') {
return new ConcreteProductA();
} else {
return new ConcreteProductB();
}
}
}
Observer Pattern:
The Observer pattern allows objects to subscribe and receive updates from a subject.
Example:
interface Observer {
update: (data: any) => void;
}
class ConcreteObserver implements Observer {
public update(data: any): void {
console.log(`Received data: ${data}`);
}
}
class Subject {
private observers: Observer[] = [];
public subscribe(observer: Observer): void {
this.observers.push(observer);
}
public unsubscribe(observer: Observer): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
public notify(data: any): void {
for (const observer of this.observers) {
observer.update(data);
}
}
}
Decorator pattern:
Decorators in TypeScript are a way to perform meta-programming. They are special kinds of declarations that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression
, where expression
must evaluate to a function that will be called at runtime with information about the decorated declaration.
Code Example:
function Component(label: string) {
return function(target: Function) {
target.prototype.componentLabel = label;
}
}
@Component('My Component')
class SomeComponent {
// This class now has metadata associated with it.
}
Module pattern:
Modules in TypeScript support the module design pattern by allowing encapsulation of functionalities. A module can contain both public and private members, thus you can choose what to expose.
Code Example:
// someModule.ts
function privateFunction() {
console.log('This is private.');
}
export function publicFunction() {
console.log('This is public.');
}
Builder pattern:
The builder pattern in TypeScript can be implemented using classes that have methods for setting properties and a final method for returning the constructed object. This pattern helps to construct complex objects by specifying types and properties step by step.
Code Example:
class QueryBuilder {
private query: Record<string, string | number> = {};
public setTable(table: string): QueryBuilder {
this.query.table = table;
return this;
}
public setLimit(limit: number): QueryBuilder {
this.query.limit = limit;
return this;
}
public build(): string {
return `SELECT * FROM ${this.query.table} LIMIT ${this.query.limit}`;
}
}
const query = new QueryBuilder().setTable('users').setLimit(10).build();
// The query is "SELECT * FROM users LIMIT 10"
Prototype Pattern:
The Prototype pattern involves creating new objects by duplicating an existing one, known as the prototype. TypeScript can achieve this using the Object.create()
method. This approach is especially beneficial when the cost of creating an object from scratch is higher than that of cloning an existing one.
Example:
class Prototype {
public prototypeField: string;
clone(): this {
return Object.create(this);
}
}
const concretePrototype = new Prototype();
concretePrototype.prototypeField = 'Prototype Value';
const clonedPrototype = concretePrototype.clone();
Strategy Pattern:
The Strategy pattern defines a set of algorithms, encapsulates them, and makes them interchangeable within the context of a client. In TypeScript, you can ensure that each strategy conforms to a common interface, allowing for the strategies to be swapped seamlessly. This is useful for changing an object's behavior dynamically.
Example:
interface Strategy {
execute(data: string): void;
}
class ConcreteStrategyA implements Strategy {
execute(data: string): void {
console.log(`Algorithm A with data: ${data}`);
}
}
class ConcreteStrategyB implements Strategy {
execute(data: string): void {
console.log(`Algorithm B with data: ${data}`);
}
}
class Context {
private strategy: Strategy;
constructor(strategy: Strategy) {
this.strategy = strategy;
}
setStrategy(strategy: Strategy) {
this.strategy = strategy;
}
executeStrategy(data: string) {
this.strategy.execute(data);
}
}
State Pattern:
The State pattern enables an object to change its behavior when its internal state changes. With TypeScript, classes and interfaces can ensure that each state has the required methods implemented. This is similar to an object transitioning between subclasses.
Example:
interface State {
handle(context: Context): void;
}
class ConcreteStateA implements State {
handle(context: Context): void {
console.log('Handling state A');
context.transitionTo(new ConcreteStateB());
}
}
class ConcreteStateB implements State {
handle(context: Context): void {
console.log('Handling state B');
context.transitionTo(new ConcreteStateA());
}
}
class Context {
private state: State;
constructor(state: State) {
this.transitionTo(state);
}
transitionTo(state: State): void {
console.log(`Transitioning to ${(<any>state).constructor.name}`);
this.state = state;
}
request(): void {
this.state.handle(this);
}
}
Adapter Pattern:
The Adapter pattern is used to allow objects with incompatible interfaces to collaborate. TypeScript's interfaces can be used to ensure that adapters comply with a specific interface for the target object. This pattern is often employed to integrate new functionalities into existing systems without modifying those systems.
Example:
interface Target {
request(): string;
}
class Adaptee {
public specificRequest(): string {
return '.eetpadA eht fo roivaheb laicepS';
}
}
class Adapter implements Target {
private adaptee: Adaptee;
constructor(adaptee: Adaptee) {
this.adaptee = adaptee;
}
request(): string {
const result = this.adaptee.specificRequest().split('').reverse().join('');
return `Adapter: (TRANSLATED) ${result}`;
}
}These design patterns are instrumental in creating maintainable and scalable applications:
Facade Pattern:
The Facade pattern provides a simplified interface to a more complex body of code, such as a library or a framework.
Example:
class ComplexSystem {
methodOne(): void { /* ... */ }
methodTwo(): void { /* ... */ }
}
class Facade {
protected subsystem: ComplexSystem;
constructor(subsystem: ComplexSystem) {
this.subsystem = subsystem || new ComplexSystem();
}
operate(): void {
this.subsystem.methodOne();
this.subsystem.methodTwo();
}
}
Composite Pattern:
The Composite pattern allows you to compose objects into tree structures to represent part-whole hierarchies.
Example:
interface Component {
operation(): void;
}
class Leaf implements Component {
public operation(): void {
// Leaf operation
}
}
class Composite implements Component {
private children: Component[] = [];
public add(child: Component): void {
this.children.push(child);
}
public operation(): void {
// Composite operation
for (const child of this.children) {
child.operation();
}
}
}
Command Pattern:
The Command pattern encapsulates a request as an object, thereby allowing for parameterization and queuing of requests.
Example:
interface Command {
execute(): void;
}
class ConcreteCommand implements Command {
private receiver: Receiver;
constructor(receiver: Receiver) {
this.receiver = receiver;
}
public execute(): void {
this.receiver.action();
}
}
class Receiver {
public action(): void {
// Action to be performed
}
}
class Invoker {
private command: Command;
public setCommand(command: Command): void {
this.command = command;
}
public executeCommand(): void {
this.command.execute();
}
}
Iterator pattern:
The Iterator pattern is useful for providing a standard way to loop through collections without exposing the underlying representation. TypeScript implements this pattern using the Iterable
and Iterator
interfaces, which can be used to define custom iterators for any class.
Code Example:
class ArrayIterator<T> implements Iterator<T> {
private cursor = 0;
constructor(private array: T[]) {}
public next(): IteratorResult<T> {
if (this.cursor < this.array.length) {
return { done: false, value: this.array[this.cursor++] };
} else {
return { done: true, value: undefined };
}
}
}
class IterableCollection<T> implements Iterable<T> {
constructor(private items: T[]) {}
public [Symbol.iterator](): Iterator<T> {
return new ArrayIterator(this.items);
}
}
const collection = new IterableCollection([1, 2, 3]);
for (const item of collection) {
console.log(item); // Outputs 1, 2, 3
}
Mediator pattern:
The Mediator pattern promotes loose coupling by ensuring that instead of components referring to each other explicitly, their interaction is handled through a mediator. This pattern can be strongly typed in TypeScript, which helps ensure that the components communicate through well-defined interfaces.
Code Example:
interface Mediator {
notify(sender: object, event: string): void;
}
class ConcreteMediator implements Mediator {
constructor(private component1: Component1, private component2: Component2) {
component1.setMediator(this);
component2.setMediator(this);
}
public notify(sender: object, event: string): void {
if (event === 'A') {
console.log('Mediator reacts on A and triggers following operations:');
this.component2.doC();
}
if (event === 'D') {
console.log('Mediator reacts on D and triggers following operations:');
this.component1.doB();
}
}
}
class Component1 {
constructor(private mediator: Mediator) {}
public doA(): void {
console.log('Component 1 does A.');
this.mediator.notify(this, 'A');
}
public doB(): void {
console.log('Component 1 does B.');
this.mediator.notify(this, 'B');
}
public setMediator(mediator: Mediator): void {
this.mediator = mediator;
}
}
class Component2 {
constructor(private mediator: Mediator) {}
public doC(): void {
console.log('Component 2 does C.');
this.mediator.notify(this, 'C');
}
public doD(): void {
console.log('Component 2 does D.');
this.mediator.notify(this, 'D');
}
public setMediator(mediator: Mediator): void {
this.mediator = mediator;
}
}
// Usage
const c1 = new Component1(null);
const c2 = new Component2(null);
const mediator = new ConcreteMediator(c1, c2);
c1.doA(); // This will trigger doC on Component2 through the mediator
c2.doD(); // This will trigger doB on Component1 through the mediator