
๐ฏ 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
orswitch
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
andQuickSort
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.