# Switch Statements

> The same switch or if-chain over a type code is repeated across the codebase, so adding one case means editing every copy.

- kind: smell
- category: oo-abusers
- aliases: Repeated Switch, Type Switching
- canonical: https://aipatterns.dev/refactoring/smells/switch-statements
- inspired-by: https://refactoring.guru/smells/switch-statements

## Signs

- Two or more functions switch on the same discriminator (`plan.tier`, `shape.kind`, `event.type`) with the same set of cases.
- Adding a new kind requires a search for every `case "pro":` in the codebase.
- A `default:` branch throws "unknown type", which is the only thing protecting you from a forgotten case.
- The switch lives in a class that does not own the data it switches on.

A single switch in one place, such as a factory or a parser, is not the smell. Duplication of the switch is.

## Why it happens

A switch is the most direct way to express "behave differently for each kind", and the first one is fine. Then a second feature needs per-kind behavior, and copying the switch is faster than reorganizing the code. Each copy is reasonable on its own; together they scatter the definition of each kind across the codebase.

## Example

Subscription tiers handled by switches in pricing, limits and display code.

```ts title="plans.ts"
type Tier = "free" | "pro" | "team";

function monthlyPriceCents(tier: Tier, seats: number): number {
  switch (tier) {
    case "free": return 0;
    case "pro": return 1200;
    case "team": return 900 * Math.max(seats, 3);
  }
}

function storageLimitGb(tier: Tier): number {
  switch (tier) {
    case "free": return 2;
    case "pro": return 100;
    case "team": return 1000;
  }
}

function badge(tier: Tier): string {
  switch (tier) {
    case "free": return "";
    case "pro": return "PRO";
    case "team": return "TEAM";
  }
}
```

Each tier becomes one object that owns all of its behavior. Adding an "enterprise" tier is one new entry, and the compiler flags any missing member.

```ts title="plans.ts"
interface Plan {
  monthlyPriceCents(seats: number): number;
  storageLimitGb: number;
  badge: string;
}

const plans: Record<Tier, Plan> = {
  free: { monthlyPriceCents: () => 0, storageLimitGb: 2, badge: "" },
  pro: { monthlyPriceCents: () => 1200, storageLimitGb: 100, badge: "PRO" },
  team: { monthlyPriceCents: (seats) => 900 * Math.max(seats, 3), storageLimitGb: 1000, badge: "TEAM" },
};

const price = plans[account.tier].monthlyPriceCents(account.seats);
```

## Treatment

- First isolate each switch with [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md), then use [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) to put it on the class that owns the discriminator.
- If the discriminator is a type code fixed at creation, use [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md), then [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md).
- If the type can change during the object's life (an account upgrades), use [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md).
- If each case calls the same method with a different argument, use [Replace Parameter with Explicit Methods](https://aipatterns.dev/refactoring/techniques/replace-parameter-with-explicit-methods.md).
- If one of the cases is "null" or "none", use [Introduce Null Object](https://aipatterns.dev/refactoring/techniques/introduce-null-object.md).
- In TypeScript, Rust, Kotlin, Swift and Scala, a discriminated union plus an exhaustiveness check (`never` in the default branch, or a `match`) makes a missing case a compile error. That can be enough on its own when the set of kinds is closed.

## Payoff

- Each kind is defined in one place, so adding a kind is one change.
- Forgotten cases become compile errors instead of runtime exceptions.
- Per-kind behavior is testable without constructing the other kinds.

## When to ignore

- There is exactly one switch over the type, typically in a factory that picks which object to create. That is where the decision belongs.
- The set of cases is closed and small, and the language checks exhaustiveness. A `match` over a sealed union is idiomatic in functional code and often clearer than a class hierarchy.
- You add new *operations* far more often than new *kinds*. Polymorphism makes kinds cheap and operations expensive; the switch makes the opposite trade. For a closed set of kinds with many operations, see [Visitor](https://aipatterns.dev/design-patterns/visitor.md) or keep the switches with exhaustiveness checks.

## Agent checklist

- [ ] You counted the switches on the same discriminator; one switch alone is not a reason to refactor.
- [ ] You decided whether kinds or operations change more often before choosing polymorphism over exhaustive matching.
- [ ] The remaining single decision point (factory or lookup) is the only place that maps the raw code to an object.
- [ ] A test exists per kind for each behavior that moved.
- [ ] A missing case is now caught by the compiler or by an interface the new kind must implement.
