๐ŸŽฏ Strategy Pattern in JavaScript/TypeScript

Design patterns are powerful tools that help us write clean, maintainable, and scalable code. One such pattern is the Strategy Pattern, which promotes flexibility by encapsulating algorithms separately and making them interchangeable.

In this blog, weโ€™ll break down the Strategy Pattern in JavaScript and TypeScript, starting from the basics and moving toward more advanced usage with real-world examples.


๐Ÿš€ What is the Strategy Pattern?

The Strategy Pattern is a behavioral design pattern that lets you define a family of algorithms, put each of them in a separate class, and make their objects interchangeable.

Why use it?

  • To avoid if-else or switch hell.
  • To encapsulate different behaviors (algorithms) behind a common interface.
  • To follow the Open/Closed Principle (open for extension, closed for modification).

๐Ÿ”ฐ Basic Implementation in JavaScript

Let's start with a simple example: a context that performs different types of sorting algorithms.

// Context
class Sorter {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  sort(data) {
    return this.strategy.sort(data);
  }
}

// Strategy A
class BubbleSort {
  sort(data) {
    console.log("Using Bubble Sort");
    // Dummy implementation
    return data.sort();
  }
}

// Strategy B
class QuickSort {
  sort(data) {
    console.log("Using Quick Sort");
    // Dummy implementation
    return data.sort(); // Assume optimized quick sort
  }
}

// Usage
const sorter = new Sorter(new BubbleSort());
console.log(sorter.sort([5, 2, 9, 1]));

sorter.setStrategy(new QuickSort());
console.log(sorter.sort([5, 2, 9, 1]));

๐Ÿ” Explanation

  • Sorter is the context class that uses a strategy.
  • BubbleSort and QuickSort are interchangeable algorithms.
  • We can change the sorting behavior at runtime.

๐Ÿง‘โ€๐Ÿ’ป Strategy Pattern in TypeScript with Interfaces

Using TypeScript makes the Strategy Pattern more robust thanks to interfaces and types.

// Strategy Interface
interface SortStrategy {
  sort(data: number[]): number[];
}

// Concrete Strategy: BubbleSort
class BubbleSort implements SortStrategy {
  sort(data: number[]): number[] {
    console.log("Using Bubble Sort");
    return [...data].sort(); // Simulated
  }
}

// Concrete Strategy: QuickSort
class QuickSort implements SortStrategy {
  sort(data: number[]): number[] {
    console.log("Using Quick Sort");
    return [...data].sort(); // Simulated
  }
}

// Context Class
class Sorter {
  private strategy: SortStrategy;

  constructor(strategy: SortStrategy) {
    this.strategy = strategy;
  }

  public setStrategy(strategy: SortStrategy): void {
    this.strategy = strategy;
  }

  public sort(data: number[]): number[] {
    return this.strategy.sort(data);
  }
}

// Usage
const sorter = new Sorter(new BubbleSort());
console.log(sorter.sort([4, 1, 3, 2]));

sorter.setStrategy(new QuickSort());
console.log(sorter.sort([4, 1, 3, 2]));

โœ… TypeScript Benefits

  • Interfaces enforce contracts for strategies.
  • Type safety reduces bugs.
  • IDE support and autocomplete!

๐Ÿ’ก Real-World Use Case: Payment Processor

Imagine an app that supports multiple payment gateways (Stripe, PayPal, Crypto). Using the Strategy Pattern allows switching between them dynamically.

interface IPaymentDetails {
    ...
}
// Strategy Interface
interface PaymentStrategy {
  collectPaymentDetail(detail: IPaymentDetails): void
  validatePaymentDetail(): boolean
  pay(amount: number): void;
}

// Concrete Strategies
class StripePayment implements PaymentStrategy {
  private details: IPaymentDetails

  collectPaymentDetail(details) {
    this.details = details
  } 
  validatePaymentDetail(): boolean {
    // validate Stripe details
    return true/false
  }
  pay(amount: number): void {
    console.log(`Paid $${amount} using Stripe`);
  }
}

class PayPalPayment implements PaymentStrategy {
  private details: IPaymentDetails

  collectPaymentDetail(details) {
    this.details = details
  } 
  validatePaymentDetail(): boolean {
    // validate PayPal details
    return true/false
  }
  pay(amount: number): void {
    console.log(`Paid $${amount} using PayPal`);
  }
}

// Context
class PaymentProcessor {
  constructor(private strategy: PaymentStrategy) {}

  setStrategy(strategy: PaymentStrategy): void {
    this.strategy = strategy;
  }

  checkout(amount: number): void {
    this.strategy.collectPaymentDetail()
    if (this.strategy.validatePaymentDetail())
    this.strategy.pay(amount);
  }
}

// Usage
const payment = new PaymentProcessor(new StripePayment());
payment.checkout(100);

payment.setStrategy(new PayPalPayment());
payment.checkout(200);

payment.setStrategy(new CryptoPayment());
payment.checkout(300);

๐Ÿง  Advanced Tips

1. โœ… Use Strategy Pattern with Dependency Injection

Frameworks like NestJS or Angular support DI. You can inject strategies based on configuration or environment.

2. ๐Ÿงช Combine with Factory Pattern

Use a factory to return the right strategy dynamically based on user input or configuration.

3. ๐Ÿ“ฆ Use It in State Machines

Strategies can act as handlers for different states in a state machine architecture.


โš–๏ธ When to use

Strategy : Need to switch between interchangeable algorithms


๐Ÿงผ When NOT to Use Strategy Pattern

  • If there's only one behavior โ€” no need to overengineer.
  • If runtime flexibility isn't required.
  • For tiny projects or scripts โ€” stick to simplicity.

๐Ÿ“ฆ Summary

  • The Strategy Pattern is powerful for encapsulating behaviors.
  • JavaScript supports it dynamically; TypeScript enhances it with type safety.
  • Useful in sorting, payments, logging, UI rendering, and more.
  • It's a great alternative to condition-heavy logic blocks.

๐Ÿ› ๏ธ You Can Try It Yourself

If you'd like to experiment with the Strategy Pattern, try coding:

  • A logging system with different log strategies (console, file, remote).
  • An image processing app with different filter strategies.
  • A chatbot with interchangeable NLP strategies.