Long if/else chains and switch statements are one of the most common code smells. The Strategy pattern replaces them with interchangeable algorithms, making code open for extension without modification.

The Problem

// ❌ Every new pricing rule means modifying this function
function calculatePrice(product: Product, userType: string): number {
  let price = product.basePrice;

  if (userType === 'premium') {
    price *= 0.8;  // 20% discount
    if (product.category === 'electronics') {
      price *= 0.95;  // Extra 5% on electronics
    }
  } else if (userType === 'employee') {
    price *= 0.7;  // 30% discount
  } else if (userType === 'wholesale') {
    if (product.quantity > 100) {
      price *= 0.6;
    } else if (product.quantity > 50) {
      price *= 0.75;
    } else {
      price *= 0.85;
    }
  }
  // This grows forever...

  return price;
}

The Strategy Solution

// Define the strategy interface
interface PricingStrategy {
  calculate(product: Product): number;
}

// Each strategy encapsulates its own logic
class StandardPricing implements PricingStrategy {
  calculate(product: Product): number {
    return product.basePrice;
  }
}

class PremiumPricing implements PricingStrategy {
  calculate(product: Product): number {
    let price = product.basePrice * 0.8;
    if (product.category === 'electronics') {
      price *= 0.95;
    }
    return price;
  }
}

class EmployeePricing implements PricingStrategy {
  calculate(product: Product): number {
    return product.basePrice * 0.7;
  }
}

class WholesalePricing implements PricingStrategy {
  calculate(product: Product): number {
    const { basePrice, quantity } = product;
    if (quantity > 100) return basePrice * 0.6;
    if (quantity > 50) return basePrice * 0.75;
    return basePrice * 0.85;
  }
}

// Strategy registry
class PricingService {
  private strategies = new Map<string, PricingStrategy>();

  constructor() {
    this.strategies.set('standard', new StandardPricing());
    this.strategies.set('premium', new PremiumPricing());
    this.strategies.set('employee', new EmployeePricing());
    this.strategies.set('wholesale', new WholesalePricing());
  }

  register(name: string, strategy: PricingStrategy): void {
    this.strategies.set(name, strategy);
  }

  calculate(product: Product, userType: string): number {
    const strategy = this.strategies.get(userType) ?? this.strategies.get('standard')!;
    return strategy.calculate(product);
  }
}

Functional Strategy Pattern

In TypeScript, you don’t need classes for simple strategies. Functions work beautifully:

type SortStrategy<T> = (items: T[]) => T[];

const sortByDate: SortStrategy<Article> = (items) =>
  [...items].sort((a, b) => b.date.getTime() - a.date.getTime());

const sortByTitle: SortStrategy<Article> = (items) =>
  [...items].sort((a, b) => a.title.localeCompare(b.title));

const sortByPopularity: SortStrategy<Article> = (items) =>
  [...items].sort((a, b) => b.views - a.views);

// Usage
function displayArticles(articles: Article[], sort: SortStrategy<Article>) {
  const sorted = sort(articles);
  sorted.forEach(article => render(article));
}

displayArticles(articles, sortByPopularity);

Composable Strategies

Strategies can be combined for powerful flexibility:

interface ValidationStrategy {
  validate(value: string): ValidationResult;
}

const required: ValidationStrategy = {
  validate: (value) => 
    value.trim().length > 0
      ? { valid: true }
      : { valid: false, error: 'Field is required' },
};

const minLength = (min: number): ValidationStrategy => ({
  validate: (value) =>
    value.length >= min
      ? { valid: true }
      : { valid: false, error: `Must be at least ${min} characters` },
});

const matchesPattern = (pattern: RegExp, msg: string): ValidationStrategy => ({
  validate: (value) =>
    pattern.test(value)
      ? { valid: true }
      : { valid: false, error: msg },
});

// Compose strategies
function composeValidators(...strategies: ValidationStrategy[]): ValidationStrategy {
  return {
    validate(value: string): ValidationResult {
      for (const strategy of strategies) {
        const result = strategy.validate(value);
        if (!result.valid) return result;
      }
      return { valid: true };
    },
  };
}

// Build complex validators from simple ones
const emailValidator = composeValidators(
  required,
  minLength(5),
  matchesPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'Invalid email format'),
);

const passwordValidator = composeValidators(
  required,
  minLength(8),
  matchesPattern(/[A-Z]/, 'Must contain an uppercase letter'),
  matchesPattern(/[0-9]/, 'Must contain a number'),
);

The Strategy pattern turns rigid conditional logic into a collection of pluggable, testable, reusable algorithms.

“Define a family of algorithms, encapsulate each one, and make them interchangeable.” — Gang of Four