Strategy Pattern in TypeScript Made Simple
What’s the Strategy Pattern?
The Strategy pattern is a behavioral design pattern. It lets you define a family of algorithms, encapsulate each one, and make them interchangeable. The Strategy pattern lets the algorithm vary independently from clients that use it.
Think about it like this: you have a job that needs to be done, and there are several different ways to do it. Instead of writing one big function with a bunch of if/else statements to choose the method, you can use the Strategy pattern. Each method becomes its own “strategy,” and you can swap them out as needed.
Why Use It?
It’s all about flexibility and maintainability.
- Flexibility: You can add new strategies without changing the existing code that uses them. This is a big win for the Open/Closed Principle.
- Maintainability: Your code becomes cleaner. Instead of bloated conditional logic, you have focused, single-responsibility classes or functions.
- Testability: Individual strategies are easier to test in isolation.
Implementing Strategy in TypeScript
Let’s use a simple example: a Calculator that can perform different arithmetic operations.
First, we define an interface for our strategies. This interface will outline the common method that all concrete strategies must implement.
interface CalculationStrategy { execute(a: number, b: number): number;}Now, let’s create some concrete strategies that implement this interface.
Addition Strategy:
class AddStrategy implements CalculationStrategy { execute(a: number, b: number): number { return a + b; }}Subtraction Strategy:
class SubtractStrategy implements CalculationStrategy { execute(a: number, b: number): number { return a - b; }}Multiplication Strategy:
class MultiplyStrategy implements CalculationStrategy { execute(a: number, b: number): number { return a * b; }}Next, we create the Context class. This is the class that will use our strategies. It holds a reference to a CalculationStrategy object and delegates the execution to it.
class Calculator { private strategy: CalculationStrategy;
constructor(strategy: CalculationStrategy) { this.strategy = strategy; }
setStrategy(strategy: CalculationStrategy) { this.strategy = strategy; }
calculate(a: number, b: number): number { return this.strategy.execute(a, b); }}Putting It All Together
Now we can use our Calculator with different strategies.
// Create instances of strategiesconst add = new AddStrategy();const subtract = new SubtractStrategy();const multiply = new MultiplyStrategy();
// Initialize calculator with a strategylet calculator = new Calculator(add);
console.log(`10 + 5 = ${calculator.calculate(10, 5)}`); // Output: 10 + 5 = 15
// Change the strategycalculator.setStrategy(subtract);console.log(`10 - 5 = ${calculator.calculate(10, 5)}`); // Output: 10 - 5 = 5
// Change againcalculator.setStrategy(multiply);console.log(`10 * 5 = ${calculator.calculate(10, 5)}`); // Output: 10 * 5 = 50Functional Approach (TypeScript Bonus)
TypeScript’s flexibility also allows for a more functional approach, which can sometimes be simpler for straightforward cases. Instead of classes, you can use functions.
// No interface needed here, just function signatures
type CalculationFunction = (a: number, b: number) => number;
const addFunc: CalculationFunction = (a, b) => a + b;const subtractFunc: CalculationFunction = (a, b) => a - b;const multiplyFunc: CalculationFunction = (a, b) => a * b;
class CalculatorFunc { private strategy: CalculationFunction;
constructor(strategy: CalculationFunction) { this.strategy = strategy; }
setStrategy(strategy: CalculationFunction) { this.strategy = strategy; }
calculate(a: number, b: number): number { return this.strategy(a, b); }}
// Usage is similarlet calcFunc = new CalculatorFunc(addFunc);console.log(`Functional 10 + 5 = ${calcFunc.calculate(10, 5)}`); // Output: Functional 10 + 5 = 15
calcFunc.setStrategy(subtractFunc);console.log(`Functional 10 - 5 = ${calcFunc.calculate(10, 5)}`); // Output: Functional 10 - 5 = 5This functional version achieves the same goal with potentially less boilerplate for simple operations. The choice between object-oriented and functional often comes down to the complexity of the strategies and the overall project architecture.
When To Consider It?
- When you have multiple variations of an algorithm for a specific task.
- When you want to avoid exposing complex, algorithm-specific data and logic to the client code.
- When an algorithm changes frequently, and you want to isolate those changes.
Conclusion
The Strategy pattern is a valuable tool in your TypeScript development arsenal. It promotes clean, flexible, and maintainable code by decoupling algorithms from the context that uses them. Whether you prefer an object-oriented or a more functional style, TypeScript makes implementing this pattern straightforward and effective. Give it a try in your next project!