Skip to content

Replace Conditional with Polymorphism

Take a switch (or if/else chain) that branches on a value’s type and give each type its own class with its own version of the method. The branch labels become subclasses, the case bodies become overriding methods, and the dispatch that used to be hand-written is now done by the language at the call site.

This is the cure for a type switch that recurs. The signal is not one switch — it is the same switch on the same type field appearing in several functions: plumage, airSpeed, singing, each re-listing every bird type. Every time you add a type, you must find and edit all of them, and it is easy to miss one. Polymorphism gathers everything one type does into one class, so adding a type means adding a class — not hunting down scattered cases.

A bird’s plumage depends on its species. Before, one switch on a type tag handles every species. After, each species is a subclass overriding plumage.

// Before
function plumage(bird: Bird): string {
switch (bird.type) {
case 'EuropeanSwallow':
return 'average';
case 'AfricanSwallow':
return bird.numberOfCoconuts > 2 ? 'tired' : 'average';
case 'NorwegianBlue':
return bird.voltage > 100 ? 'scorched' : 'beautiful';
default:
return 'unknown';
}
}
// After
abstract class Bird {
abstract plumage(): string;
}
class EuropeanSwallow extends Bird {
plumage(): string {
return 'average';
}
}
class AfricanSwallow extends Bird {
constructor(private numberOfCoconuts: number) {
super();
}
plumage(): string {
return this.numberOfCoconuts > 2 ? 'tired' : 'average';
}
}
class NorwegianBlue extends Bird {
constructor(private voltage: number) {
super();
}
plumage(): string {
return this.voltage > 100 ? 'scorched' : 'beautiful';
}
}
flowchart TD
  subgraph Before["Before — one switch, repeated"]
    A["plumage(bird):<br/>switch bird.type<br/>case EuropeanSwallow ...<br/>case AfricanSwallow ...<br/>case NorwegianBlue ..."]
  end
  subgraph After["After — polymorphic classes"]
    B["Bird.plumage()"]
    B --> C["EuropeanSwallow.plumage()"]
    B --> D["AfricanSwallow.plumage()"]
    B --> E["NorwegianBlue.plumage()"]
  end
  Before -.->|"Replace Conditional with Polymorphism"| After
A recurring type switch becomes a base type and one subclass per case
  1. Create a base type (class, interface, or trait) with the method that the switch currently computes.
  2. Create one subclass per branch label. Move each case body into that subclass’s override, replacing references to the type field with the subclass’s own data.
  3. Replace construction sites so the right subclass is created instead of a tagged value — often via a small factory.
  4. Turn the original switch into a single call to the polymorphic method. Run your tests.
  5. Repeat for the next function that switched on the same type, deleting each switch as its logic moves into the subclasses. Run tests after every method you move.

Use this sparingly — it earns its keep only when the same conditional on a type appears in more than one place, so that adding a type today means editing several functions. That recurrence is what a class hierarchy removes.

The cost is real: you introduce a hierarchy, a factory, and more indirection. For a single switch that lives in one function, polymorphism is overkill — a plain switch is clearer, and Decompose Conditional or guard clauses serve better. Add the classes only once the repetition proves the structure is worth it.

What is the strongest signal that you should reach for Replace Conditional with Polymorphism?
What do the branch labels of the switch become?
After moving the logic into subclasses, what replaces the original switch?
When is this refactoring overkill?