
Stop Writing Huge Switch-Cases: Use Strategy Pattern
Learn how to refactor growing switch-case and if-else statements in TypeScript using the Strategy Pattern to make your code more maintainable and testable.
Are you struggling with massive switch-case statements that grow out of control every time a new feature is added? In this article, we will explore how to use the Strategy Pattern in TypeScript to break down these monolithic blocks into clean, testable, and maintainable code.
We have all been there. You start a new project and need to implement a simple payment processor. You write a neat little switch-case statement to handle credit_card and paypal. It takes twenty lines of code. Life is good.
Fast forward a year. The business has grown, and your neat little function is now a 500-line monolithic beast handling Apple Pay, Google Pay, bank transfers, crypto wallets, and buy-now-pay-later services. Every time you need to add a new payment method, you have to carefully crack open this giant file, scroll down to line 342, and insert a new case.
This is a classic trap in software development. As developers, we often let simple control structures grow into unmaintainable "God functions." Today, we are going to look at how to stop this anti-pattern.
The Problem with the Expanding Switch-Case
Why is a massive switch-case or if-else chain so problematic?
- Violating the Open-Closed Principle (OCP): The OCP states that software entities should be open for extension but closed for modification. When you add a new payment method, you are actively modifying the core
processPaymentfunction. This increases the risk of accidentally breaking existing logic. - Merge Conflict Hell: If multiple developers are working on different features that require adding new cases, they will all be modifying the same function in the same file. Git will complain, and resolving these conflicts is error-prone.
- Testing Nightmares: To write a unit test for just the PayPal branch, you have to mock dependencies that might only be used by the Stripe or Crypto branches. The setup for your test becomes bloated and fragile.
The Functional Approach: Object Maps

In JavaScript and TypeScript, we don't always need heavyweight Object-Oriented architecture to implement the Strategy Pattern. We can use functional programming concepts—specifically, using objects or Maps as dictionaries for our strategies.
Let's look at the "Before" code:
type PaymentMethod = 'stripe' | 'paypal' | 'crypto';
async function processPayment(method: PaymentMethod, amount: number) {
switch (method) {
case 'stripe':
// 50 lines of Stripe setup and API calls
console.log(`Processing $${amount} via Stripe...`);
break;
case 'paypal':
// 40 lines of PayPal logic
console.log(`Processing $${amount} via PayPal...`);
break;
case 'crypto':
// 60 lines of Web3 logic
console.log(`Processing $${amount} via Crypto...`);
break;
default:
throw new Error('Unknown payment method');
}
}
Instead of a switch statement, we can extract each block of logic into its own function and map them using a TypeScript Record:
type PaymentMethod = 'stripe' | 'paypal' | 'crypto';
type PaymentHandler = (amount: number) => Promise<void>;
const handleStripe: PaymentHandler = async (amount) => {
console.log(`Processing $${amount} via Stripe...`);
};
const handlePaypal: PaymentHandler = async (amount) => {
console.log(`Processing $${amount} via PayPal...`);
};
const handleCrypto: PaymentHandler = async (amount) => {
console.log(`Processing $${amount} via Crypto...`);
};
// The Strategy Map
const paymentStrategies: Record<PaymentMethod, PaymentHandler> = {
stripe: handleStripe,
paypal: handlePaypal,
crypto: handleCrypto,
};
async function processPayment(method: PaymentMethod, amount: number) {
const handler = paymentStrategies[method];
if (!handler) {
throw new Error(`No strategy found for ${method}`);
}
await handler(amount);
}
This simple refactoring brings immense benefits. The processPayment function is now closed for modification. If you need to add apple_pay, you simply write a handleApplePay function and add it to the paymentStrategies record.
Retaining Exhaustive Checks
One reason developers love switch statements in TypeScript is the ability to do exhaustive type checking. If you add a new value to the PaymentMethod union type, you want TypeScript to yell at you if you forget to handle it.
Our Record-based approach does exactly that! Because we typed the map as Record<PaymentMethod, PaymentHandler>, if you add 'apple_pay' to the PaymentMethod type, TypeScript will throw a compile-time error on the paymentStrategies object until you provide the corresponding handler. You get full type safety without the visual clutter.
The Object-Oriented Approach: Classes and Interfaces

If your strategies are complex and require their own internal state or external dependencies (like a database connection, a logger, or an HTTP client), the Object-Oriented implementation of the Strategy Pattern is often cleaner.
First, define a common interface that all strategies must adhere to:
interface IPaymentStrategy {
pay(amount: number): Promise<boolean>;
}
Next, implement concrete classes for each strategy:
class StripeStrategy implements IPaymentStrategy {
constructor(private readonly apiKey: string) {}
async pay(amount: number): Promise<boolean> {
// Complex Stripe API logic here
console.log(`Stripe payment with key ${this.apiKey}`);
return true;
}
}
class PaypalStrategy implements IPaymentStrategy {
constructor(private readonly clientId: string) {}
async pay(amount: number): Promise<boolean> {
// Complex PayPal API logic here
console.log(`PayPal payment with client ${this.clientId}`);
return true;
}
}
Finally, you use a Context class or a Dependency Injection container to execute the chosen strategy. In modern Node.js backends like NestJS, you would inject these strategies directly. Here is a simple manual Context approach:
class PaymentProcessor {
private strategy: IPaymentStrategy;
constructor(strategy: IPaymentStrategy) {
this.strategy = strategy;
}
setStrategy(strategy: IPaymentStrategy) {
this.strategy = strategy;
}
async executePayment(amount: number) {
return await this.strategy.pay(amount);
}
}
// Execution
const processor = new PaymentProcessor(new StripeStrategy('sk_test_...'));
await processor.executePayment(100);
// Switch strategy dynamically at runtime
processor.setStrategy(new PaypalStrategy('client_...'));
await processor.executePayment(200);
The Ultimate Benefit: Isolated Testing
Whether you choose the functional map or the OOP interface, the biggest win is testability.
In the old switch-case world, writing a unit test for PayPal meant importing the giant file, dealing with Stripe's mocked dependencies that you didn't even care about, and navigating the complex setup.
With the Strategy Pattern, PaypalStrategy (or handlePaypal) lives in its own isolated file. You can write comprehensive unit tests focusing solely on its behavior. The core processPayment function only needs a single test to verify that it correctly invokes the mapped strategy.
When NOT to use the Strategy Pattern
Like all design patterns, Strategy is a tool, not a religion. Do not blindly apply it everywhere.
If your switch-case or if-else block handles simple UI state (like light, dark, and system themes) and has only two or three branches returning scalar values, extracting them into strategies is overengineering. It adds unnecessary boilerplate and reduces readability.
A good rule of thumb is the Rule of Three: Wait until you have three distinct cases, or until the individual blocks of logic become complex enough to warrant their own isolated testing, before you refactor into a Strategy.
Conclusion
Massive conditional blocks are a breeding ground for bugs, merge conflicts, and technical debt. By recognizing when a switch-case has outgrown its usefulness and refactoring it into a Strategy Pattern—either functionally with maps or structurally with OOP classes—you make your TypeScript code robust, extensible, and a joy to test.
Stop putting all your logic in one basket. Divide, conquer, and let the strategies do the heavy lifting.
written by
Nguyên Tech
Responses
Loading comments…