Polymorphism
Different objects, same call — code written against a shared interface works with any implementation without knowing the specific type.
Intent & Description
🎯 Intent
Write code once that works with any implementation of an interface — add new types without touching the calling code.
📋 Context
You have a list of Shape objects — Rectangle, Circle, Triangle. You want to call area() on each without type-checking which kind it is. Without polymorphism, that’s an if-else chain that grows with every new shape you add.
💡 Solution
Define a shared interface or base class with the methods that must be implemented. Concrete classes provide their own implementations. Calling code is written against the interface only — it works with any concrete type that honors the contract, including ones added later.
Real-world Use Case
📌 TL;DR
Same call, different behavior per type. Eliminates type-switch chains and makes calling code oblivious to concrete implementations.
Advantages
- Calling code is isolated from concrete types — add new implementations without changing callers
- Replaces brittle if-else-instanceof chains with extensible, open-closed design
- Behavior is determined at runtime by the actual object — flexible and late-bound
Disadvantages
- Actual behavior can be less obvious — you need to know the runtime type to understand what runs
- Complex class hierarchies make the polymorphic chain harder to follow
- Runtime dispatch has a small performance cost in hot paths
class Shape {
area() {
throw new Error('Must implement area method');
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
area() {
return Math.PI * this.radius ** 2;
}
}
const shapes = [new Rectangle(5, 3), new Circle(2)];
shapes.forEach(shape => console.log(shape.area()));