Typescript Foundations / Best Practices and Real World Patterns

Typescript Design patterns

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