This file demonstrates how to replace a switch
statement with polymorphism, one of the core principles of Object-Oriented Programming (OOP). The goal is to improve code readability, maintainability, and extensibility by avoiding the pitfalls of switch
statements.
Why Avoid switch
Statements 🤔?
- Long and Complex Methods: switch statements often lead to lengthy methods with dozens of conditions, making the code harder to read and maintain.
- Lack of Extensibility: Adding new cases to a
switch
statement requires modifying the existing code, which violates the Open/Closed Principle (OCP) of software design. - Mixed Logic: A single
switch
statement can mix unrelated logic, violating the Single Responsibility Principle (SRP). - Unreadable Code: Sifting through hundreds of conditionals is not user-friendly and can confuse other developers.
By leveraging polymorphism, we can eliminate the need for switch
statements. Each case in the switch
is replaced with a class that encapsulates the behavior for that specific case. This approach adheres to clean code principles and makes the code easier to extend and maintain.
Imagine you have a scenario where you need to create different types of fruit objects based on user input or some other condition.
Here what would the initial code look like using a switch
statement:
enum FruitType {
Apple = 'Apple',
Banana = 'Banana',
Lemon = 'Lemon',
Orange = 'Orange',
}
function getPrice(fruit: FruitType): number {
let result: number;
switch (fruit) {
case Fruits.Banana:
result = 11.1;
break;
case Fruits.Lemon:
result = 6.5;
break;
case Fruits.Orange:
result = 7.7;
break;
case Fruits.Apple:
result = 5.2;
break;
default:
result = 0;
}
return result;
}
As you can see, the switch
statement is long and complex. It also violates the Open/Closed Principle because adding a new fruit type would require modifying the existing code.
Instead, we can use polymorphism to create a cleaner and more maintainable solution.
We define an abstract Fruit class that encapsulates the shared behavior of all fruits. Each specific fruit (e.g., Apple
, Banana
, Lemon
, Orange
) is implemented as a subclass of Fruit
, overriding the necessary properties or methods.
abstract class Fruit implements Nullable {
protected constructor(readonly price: number) {}
}
We can now leverage the Registry Pattern to create instances of the specific fruit classes based on user input or other conditions. This allows us to avoid the switch
statement entirely.
export const Fruits: Record<FruitType, new () => Fruit> = {
Apple: class extends Fruit {
constructor() {
super(5.2);
}
},
Banana: class extends Fruit {
constructor() {
super(11.1);
}
},
Lemon: class extends Fruit {
constructor() {
super(6.5);
}
},
Orange: class extends Fruit {
constructor() {
super(7.7);
}
},
};
function findFruitConstructor(
constructorName: string | FruitType
): new () => Fruit {
const fruitType = Object.values(FruitType).find(
(type) => type === constructorName
) as FruitType | undefined;
return fruitType ? Fruits[fruitType] : NullableFruit;
}
Instead of using a conditional statement to determine the behavior based on the fruit type, we use a findFruitConstructor
function to dynamically retrieve the appropriate class constructor. This allows us to create instances of the desired fruit without hardcoding logic.
const Banana = findFruitConstructor('Banana');
const banana = new Banana();
console.log(banana); // { "price": 11.1 }
console.log(banana.isNull()); // false
// Won't throw an error
const NotExisting = findFruitConstructor('NotExisting');
const notExistingFruit = new NotExisting();
console.log(notExistingFruit); // { "price": 0 }
console.log(notExistingFruit.isNull()); // true
This pattern combines several design patterns and TypeScript features. It's a combination of:
-
Registry Pattern - The
Fruits
object acts as a registry that maps fruit types to their constructor functions. -
Null Object Pattern - The
NullableFruit
class represents a "null" implementation of theFruit
abstraction, allowing you to handle missing fruits without throwing errors or checking for null/undefined. -
Factory Method Pattern - The
findFruitConstructor
function acts as a factory that retrieves the appropriate constructor based on a string identifier.
The overall approach might be referred to as a "Type-Safe Registry with Null Object" pattern. It allows you to:
- Maintain a strongly-typed registry of constructors
- Look up constructors by string identifier
- Handle missing values gracefully with a default implementation
- Provide a consistent interface across all implementations
- Check if an object is a "null" implementation using the
isNull()
method
This is a particularly TypeScript-oriented implementation that leverages TypeScript's enum system, class inheritance, and the Record
utility type to create a robust type-safe registry with built-in null handling.
- Medium article: https://obaranovskyi.medium.com/typescript-use-polymorphism-in-place-of-the-switch-and-other-conditionals-1cfcc705bcc1
- Registry Pattern: https://dev.to/walosha/registry-pattern-revolutionize-your-object-creation-and-management-lms-as-a-case-study-58km
- Factory Method Pattern: https://refactoring.guru/design-patterns/factory-method
Here is the full code 👇