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.

6 min read

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.

javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
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.

Method dispatch in polymorphic call

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.

javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
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

ApproachBest when
Method overriding (extends)A clear "is-a" relationship exists, such as a Circle being a Shape
Duck typingObjects from unrelated sources need the same interface
CompositionBehaviors 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

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.
RunePowered by Rune AI

Frequently Asked Questions

Does JavaScript have interfaces like Java?

No. JavaScript does not have a formal interface keyword. Polymorphism relies on duck typing: if an object has the right method, you can use it. TypeScript adds compile-time interfaces, but at runtime JavaScript only cares whether the method exists.

Can I achieve polymorphism without classes?

Yes. Plain objects, factory functions, and the prototype chain all support polymorphic behavior. Any object that implements the expected method can be used polymorphically regardless of how it was created.

What is the difference between method overriding and method overloading?

Method overriding means a subclass provides its own version of a parent's method. Method overloading means multiple methods with the same name but different parameters -- JavaScript does not natively support overloading. You simulate it by checking argument types or counts inside a single method.

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.