Ad – 728Γ—90
πŸ—οΈ OOP

JavaScript Polymorphism – One Interface, Many Implementations

Polymorphism β€” from Greek for "many forms" β€” lets you write code that works on many different object types through a single consistent interface. In JavaScript this manifests as method overriding (child classes redefine parent methods), duck typing (if it has the right methods, it works), and overriding well-known methods like toString() and valueOf(). Mastering polymorphism leads to more flexible, extensible designs.

⏱️ 18 min read 🎯 Intermediate πŸ“… Updated 2026

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.
ℹ️
Polymorphism enables the Open/Closed Principle

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.

JavaScript
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()));
β–Ά Output
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.

JavaScript
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)}`);
β–Ά Output
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.

JavaScript
// 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));
β–Ά Output
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.

JavaScript
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
β–Ά Output
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.

RoleResponsibilityWho defines it
Template methodDefines algorithm skeleton; calls hook methodsBase class (do not override)
Hook methodsSteps that vary per subclassOverridden by subclasses
Concrete methodFixed step; same for all subclassesBase class
πŸ’‘
Hollywood Principle

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

⚠️
Duck typing risks

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.

Ad – 336Γ—280

πŸ‹οΈ Practical Exercise

Implement a payment processing system with polymorphism:

  • Base class PaymentMethod with abstract charge(amount) and refund(amount).
  • Subclasses: CreditCard, PayPal, CryptoCurrency β€” each with different fee logic and toString().
  • Write a processPayments(payments, methods) function that zips payments with methods and calls the right charge() on each β€” purely duck typed, no instanceof.

πŸ”₯ 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() vs valueOf()?
  • 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; override valueOf() 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

Does JavaScript support function overloading (same name, different parameters)? +

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 { ... }.

How do I make a method truly non-overridable in JavaScript? +

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.

What is the difference between overriding and overloading? +

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.