What Is Polymorphism?
Polymorphism means the same interface (method name or operator) produces different behaviour depending on the type of object it is called on. JavaScript achieves this through:
- Subtype polymorphism β child classes override parent methods.
- Duck typing β any object with the right method signature is accepted.
- Ad-hoc polymorphism β function overloading via argument inspection.
Your code is open for extension (add new subclasses) but closed for modification (existing code does not change). You can add a new shape to a drawing engine without touching any existing shape code.
Method Overriding in JS Classes
When a child class declares a method with the same name as a parent method, the child's version shadows the parent's. At runtime, JavaScript finds the child's method first while walking the prototype chain.
class Notification {
constructor(message) {
this.message = message;
this.timestamp = new Date().toISOString();
}
send() {
return `[${this.timestamp.slice(11,19)}] Sending: ${this.message}`;
}
format() {
return this.message; // base format β overridden by subclasses
}
}
class EmailNotification extends Notification {
constructor(message, to) {
super(message);
this.to = to;
}
format() {
return `To: ${this.to}\nSubject: Notification\n\n${this.message}`;
}
}
class SMSNotification extends Notification {
format() {
// SMS: keep it short
return this.message.length > 160
? this.message.slice(0, 157) + '...'
: this.message;
}
}
class PushNotification extends Notification {
constructor(message, badge = 1) {
super(message);
this.badge = badge;
}
format() {
return `π (${this.badge}) ${this.message}`;
}
}
// Polymorphic usage β same interface, different behaviour
const notifications = [
new EmailNotification('Your order shipped!', 'user@example.com'),
new SMSNotification('Your code: 123456'),
new PushNotification('New message from Alice', 3)
];
notifications.forEach(n => console.log(n.format()));
console.log('---');
notifications.forEach(n => console.log(n.send()));
To: user@example.com Subject: Notification Your order shipped! Your code: 123456 π (3) New message from Alice --- [hh:mm:ss] Sending: Your order shipped! [hh:mm:ss] Sending: Your code: 123456 [hh:mm:ss] Sending: New message from Alice
Shape Hierarchy β Classic Polymorphism Example
The canonical OOP example: a Shape base class defines an area() and perimeter() interface. Each concrete subclass provides its own implementation.
class Shape {
constructor(color = 'black') {
if (new.target === Shape) throw new Error('Shape is abstract');
this.color = color;
}
area() { throw new Error(`${this.constructor.name} must implement area()`); }
perimeter() { throw new Error(`${this.constructor.name} must implement perimeter()`); }
describe() {
return `${this.constructor.name} [${this.color}]: area=${this.area().toFixed(2)}, perimeter=${this.perimeter().toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(radius, color) {
super(color);
this.radius = radius;
}
area() { return Math.PI * this.radius ** 2; }
perimeter() { return 2 * Math.PI * this.radius; }
}
class Rectangle extends Shape {
constructor(width, height, color) {
super(color);
this.width = width;
this.height = height;
}
area() { return this.width * this.height; }
perimeter() { return 2 * (this.width + this.height); }
}
class Triangle extends Shape {
constructor(a, b, c, color) {
super(color);
this.a = a; this.b = b; this.c = c;
}
perimeter() { return this.a + this.b + this.c; }
area() {
const s = this.perimeter() / 2;
return Math.sqrt(s * (s - this.a) * (s - this.b) * (s - this.c));
}
}
const shapes = [
new Circle(5, 'red'),
new Rectangle(4, 6, 'blue'),
new Triangle(3, 4, 5, 'green')
];
shapes.forEach(s => console.log(s.describe()));
// Total area β polymorphic reduce
const total = shapes.reduce((sum, s) => sum + s.area(), 0);
console.log(`Total area: ${total.toFixed(2)}`);
Circle [red]: area=78.54, perimeter=31.42 Rectangle [blue]: area=24.00, perimeter=20.00 Triangle [green]: area=6.00, perimeter=12.00 Total area: 108.54
Duck Typing β "If It Quacks Like a Duck"
JavaScript is dynamically typed, so you can write functions that work with any object that has the right methods β without caring about the object's class or inheritance. This is duck typing: if it walks like a duck and quacks like a duck, treat it as a duck.
// Function only cares that the object has a .speak() method
function makeNoise(animal) {
if (typeof animal.speak !== 'function') {
throw new TypeError('Object must have a speak() method');
}
return animal.speak();
}
// These are unrelated objects β no shared class hierarchy
const dog = { name: 'Rex', speak() { return `${this.name}: Woof!`; } };
const cat = { name: 'Luna', speak() { return `${this.name}: Meow!`; } };
const duck = { name: 'Donald', speak() { return `${this.name}: Quack!`; } };
const robot = { name: 'R2D2', speak() { return `${this.name}: Beep boop!`; } };
[dog, cat, duck, robot].forEach(a => console.log(makeNoise(a)));
// Array also works if it has speak method!
const trumpet = {
name: 'Trumpet',
speak() { return `${this.name}: πΊ Toot!`; }
};
console.log(makeNoise(trumpet));
Rex: Woof! Luna: Meow! Donald: Quack! R2D2: Beep boop! Trumpet: πΊ Toot!
Overriding toString() and valueOf()
JavaScript automatically calls toString() when an object is used in a string context and valueOf() when used in a numeric context. Overriding these methods lets your custom objects participate in built-in operations polymorphically.
class Vector2D {
constructor(x, y) {
this.x = x;
this.y = y;
}
// Used in string contexts: template literals, console.log, String()
toString() {
return `Vector(${this.x}, ${this.y})`;
}
// Used in numeric contexts: +, -, *, comparisons
valueOf() {
return Math.sqrt(this.x ** 2 + this.y ** 2); // magnitude
}
add(other) {
return new Vector2D(this.x + other.x, this.y + other.y);
}
}
const v1 = new Vector2D(3, 4);
const v2 = new Vector2D(1, 2);
console.log(`${v1}`); // toString() called
console.log(String(v1)); // toString() called
console.log(+v1); // valueOf() called β magnitude = 5
console.log(v1 > v2); // valueOf() called for comparison
console.log(v1.add(v2).toString()); // add returns new Vector
Vector(3, 4) Vector(3, 4) 5 true Vector(4, 6)
Template Method Pattern
The template method is a design pattern built on polymorphism. A base class defines the skeleton of an algorithm in a final (non-overridden) method, with steps that subclasses must fill in.
| Role | Responsibility | Who defines it |
|---|---|---|
| Template method | Defines algorithm skeleton; calls hook methods | Base class (do not override) |
| Hook methods | Steps that vary per subclass | Overridden by subclasses |
| Concrete method | Fixed step; same for all subclasses | Base class |
"Don't call us, we'll call you." The base class calls the subclass's hook methods β subclasses don't orchestrate the algorithm, they just fill in the blanks. This keeps orchestration logic in one place.
Always validate that the expected method exists before calling it (typeof check or optional chaining). Relying on implicit duck typing without checks can produce cryptic "x is not a function" errors at runtime.
ποΈ Practical Exercise
Implement a payment processing system with polymorphism:
- Base class
PaymentMethodwith abstractcharge(amount)andrefund(amount). - Subclasses:
CreditCard,PayPal,CryptoCurrencyβ each with different fee logic andtoString(). - Write a
processPayments(payments, methods)function that zips payments with methods and calls the rightcharge()on each β purely duck typed, noinstanceof.
π₯ Challenge Exercise
Implement the Template Method pattern for a data exporter: base class DataExporter with a final export(data) method that calls prepare(data), transform(data), and write(output) in sequence. Create CSVExporter and JSONExporter subclasses that each override the three hook methods. The base export method also logs timing information that subclasses should not be able to remove.
Interview Questions
- What is polymorphism? Name the two main forms JavaScript supports.
- How does method overriding work via the prototype chain?
- What is duck typing and how does it relate to JavaScript's dynamic nature?
- When would you override
toString()vsvalueOf()? - What is the template method design pattern and how does it use polymorphism?
- How do you enforce that a subclass implements an abstract method in JavaScript?
π Summary
- Polymorphism lets one interface drive many different implementations.
- Method overriding: child class shadows parent method; use
super.method()to call parent. - Duck typing: any object with the right method signature works β no class hierarchy required.
- Override
toString()for string contexts; overridevalueOf()for numeric contexts. - The template method pattern uses polymorphism to define an algorithm skeleton in a base class with customisable hook steps.
Frequently Asked Questions
Not natively. Only the last function definition with a given name is kept. To simulate overloading, inspect argument types or count inside the function body: if (typeof arg === 'string') { ... } else { ... }.
There is no native final keyword. You can simulate it by checking new.target or by using Object.defineProperty with writable: false on the method. In practice, documenting the intent in JSDoc (@final) is the most common approach.
Overriding is a child class redefining an inherited method (runtime polymorphism). Overloading is multiple methods with the same name but different parameter signatures (compile-time polymorphism). JavaScript supports overriding natively; overloading must be simulated manually.