Polymorphism in JavaScript: Complete Tutorial
Polymorphism lets different objects respond to the same method name in their own way. Learn method overriding, duck typing, and how JavaScript achieves polymorphic behavior.
Polymorphism means many forms. In JavaScript it is the ability of different objects to respond to the same method name in their own way. You call .speak() on three different objects and each one produces a different sound, without ever checking which type of object you have.
class Dog {
speak() { return "Woof!"; }
}
class Cat {
speak() { return "Meow!"; }
}
const animals = [new Dog(), new Cat()];
for (const animal of animals) {
console.log(animal.speak()); // "Woof!" then "Meow!"
}The loop calls speak on each animal without checking whether the object is a Dog or a Cat. It just looks for a speak method on the object and calls it. Each class provides its own version, and that shared calling pattern is polymorphism.
Method Overriding with extends
The most common polymorphic pattern in JavaScript uses extends to create a subclass that replaces a parent method. The subclass method keeps the same name but supplies different behavior.
class Shape {
area() { return 0; }
describe() { return `Area: ${this.area()}`; }
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
area() { return Math.PI * this.radius * this.radius; }
}The Shape class defines a default area that returns 0. Circle overrides area with its own formula, while describe stays defined only on Shape.
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
area() { return this.width * this.height; }
}
const shapes = [new Circle(5), new Rectangle(4, 6)];
for (const shape of shapes) console.log(shape.describe());
// "Area: 78.53981633974483"
// "Area: 24"Rectangle overrides area the same way Circle does, with its own formula. When describe calls this.area(), JavaScript dispatches to whichever version of area belongs to the actual instance, a behavior known as dynamic dispatch.
When describe() runs this.area(), JavaScript starts at the actual instance and looks for the closest matching method. It finds area on Circle or Rectangle before ever reaching the shared Shape version. The closest definition always wins.
Using super to Extend, Not Replace
super.methodName() calls the parent's version of a method from inside the overriding one. This lets you add behavior on top of what the parent already does instead of throwing it away.
class Logger {
log(message) { return `[LOG] ${message}`; }
}
class TimestampedLogger extends Logger {
log(message) {
const timestamp = new Date().toISOString();
return `${timestamp} ${super.log(message)}`;
}
}
const logger = new TimestampedLogger();
console.log(logger.log("Server started"));
// e.g. "2026-07-15T10:30:00.000Z [LOG] Server started"TimestampedLogger calls super.log to reuse the parent's formatting, then wraps the result with a timestamp. The parent's logic is preserved, not thrown away.
Duck Typing
JavaScript does not require objects to share a parent class to be used polymorphically. If an object has the right method, you can use it, a pattern known as duck typing: if it looks like a duck and quacks like a duck, treat it as a duck.
function makeItSpeak(thing) {
if (typeof thing.speak === "function") {
return thing.speak();
}
return "This thing cannot speak";
}
const dog = { speak() { return "Woof!"; } };
const rock = { weight: 5 };
console.log(makeItSpeak(dog)); // "Woof!"
console.log(makeItSpeak(rock)); // "This thing cannot speak"The dog and rock objects share no common class or prototype. The function only checks that a speak method exists before calling it, so any object with that method works, regardless of where it came from.
Duck typing is JavaScript's natural polymorphic style. It is the reason array methods like map and filter work on any array-like object, and why a for...of loop works on any iterable.
Polymorphism in Practice: A Notification System
Here is a realistic example. Different notification channels all respond to the same send method, so calling code never needs to know which channel it is talking to.
class EmailNotifier {
send(message, recipient) {
return `Email sent to ${recipient}: "${message}"`;
}
}
class SMSNotifier {
send(message, recipient) {
return `SMS sent to ${recipient}: "${message}"`;
}
}Both notifier classes expose the same send method with the same parameters, even though the underlying delivery mechanism is completely different.
function broadcast(message, recipients, notifier) {
return recipients.map((recipient) => notifier.send(message, recipient));
}
const users = ["Alice", "Bob"];
console.log(broadcast("Sale starts now!", users, new EmailNotifier()));
// ["Email sent to Alice: ...", "Email sent to Bob: ..."]The broadcast function accepts any notifier object that has a send method with this shape. You can add a SlackNotifier or a DiscordNotifier without changing a single line in broadcast, because the calling code is decoupled from the concrete notification type.
Composition Over Inheritance
Deep inheritance chains for polymorphism can become rigid. An alternative is composition: pass behavior into an object instead of baking it into a class hierarchy.
function createSpeaker(sound) {
return { speak() { return sound; } };
}
const dog = { name: "Rex", ...createSpeaker("Woof!") };
const cat = { name: "Whiskers", ...createSpeaker("Meow!") };
console.log(dog.speak()); // "Woof!"
console.log(cat.speak()); // "Meow!"Instead of a Dog-extends-Animal chain, this composes a speak behavior into any object that needs it. That is more flexible than class-based polymorphism when behaviors need to be mixed and matched across unrelated types.
When to Use Each Approach
| Approach | Best when |
|---|---|
| Method overriding (extends) | A clear "is-a" relationship exists, such as a Circle being a Shape |
| Duck typing | Objects from unrelated sources need the same interface |
| Composition | Behaviors need to be combined across different types |
Polymorphism is about writing code that works with many shapes of objects. JavaScript offers several ways to achieve it, from classical extends to lightweight duck typing and composition. For related concepts, see class inheritance and the prototype chain.
Rune AI
Key Insights
- Polymorphism means the same method name behaves differently depending on the object it is called on.
- Method overriding with extends and super lets a subclass replace a parent's behavior while reusing the parent's logic.
- Duck typing is JavaScript's native polymorphic style: if it walks like a duck and talks like a duck, treat it as a duck.
- Use polymorphism to write code that works with many object types through a shared interface.
- Prefer composition over deep inheritance chains for polymorphic behavior.
Frequently Asked Questions
Does JavaScript have interfaces like Java?
Can I achieve polymorphism without classes?
What is the difference between method overriding and method overloading?
Conclusion
Polymorphism in JavaScript means different objects respond to the same method call in their own way. Method overriding through extends is the most common form. Duck typing is JavaScript's natural polymorphic style: you do not need a shared parent class, only a shared method signature. Both patterns let you write code that works with any object that follows the expected contract.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.