Here’s a quick guide to TypeScript Design Patterns — classic solutions adapted to benefit from TypeScript’s type safety and tooling.
🧱 Factory Pattern
Create objects without exposing the instantiation logic.
interface Animal {
speak(): void;
}
class Dog implements Animal {
speak() {
console.log("Woof!");
}
}
class Cat implements Animal {
speak() {
console.log("Meow!");
}
}
function animalFactory(type: "dog" | "cat"): Animal {
if (type === "dog") return new Dog();
return new Cat();
}
const pet = animalFactory("dog");
pet.speak(); // Woof!👷 Builder Pattern
Construct complex objects step-by-step.
class Computer {
cpu?: string;
ram?: string;
storage?: string;
}
class ComputerBuilder {
private computer: Computer = new Computer();
setCPU(cpu: string): this {
this.computer.cpu = cpu;
return this;
}
setRAM(ram: string): this {
this.computer.ram = ram;
return this;
}
build(): Computer {
return this.computer;
}
}
const pc = new ComputerBuilder().setCPU("i7").setRAM("16GB").build();👮 Singleton Pattern
Restrict class to only one instance.
class Database {
private static instance: Database;
private constructor() {}
static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
}
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true🧩 Strategy Pattern
Define multiple algorithms and switch them at runtime.
interface PaymentStrategy {
pay(amount: number): void;
}
class CreditCardPayment implements PaymentStrategy {
pay(amount: number) {
console.log(`Paid ₹${amount} with Credit Card`);
}
}
class UpiPayment implements PaymentStrategy {
pay(amount: number) {
console.log(`Paid ₹${amount} via UPI`);
}
}
class PaymentContext {
constructor(private strategy: PaymentStrategy) {}
execute(amount: number) {
this.strategy.pay(amount);
}
}
const upi = new PaymentContext(new UpiPayment());
upi.execute(500); // Paid ₹500 via UPI🔌 Adapter Pattern
Convert one interface to another expected by the client.
class OldAPI {
requestData(): string {
return "Data from old API";
}
}
interface NewAPI {
fetch(): string;
}
class APIAdapter implements NewAPI {
constructor(private oldAPI: OldAPI) {}
fetch(): string {
return this.oldAPI.requestData();
}
}
const adapter = new APIAdapter(new OldAPI());
console.log(adapter.fetch());🔄 Observer Pattern
A subject notifies observers about state changes.
type Observer = (data: string) => void;
class Subject {
private observers: Observer[] = [];
subscribe(obs: Observer) {
this.observers.push(obs);
}
notify(data: string) {
this.observers.forEach((obs) => obs(data));
}
}
const subject = new Subject();
subject.subscribe((data) => console.log("Listener 1:", data));
subject.subscribe((data) => console.log("Listener 2:", data));
subject.notify("Hello!");🧠 Bonus: TypeScript-Specific Tips
Use interfaces to enforce structure
Use enums for constant configuration
Use generics to make patterns reusable
Combine with utility types (e.g.,
Partial,Pick,Omit) for better DRY code