Polymorphism in JavaScript: Complete Tutorial
A complete guide to polymorphism in JavaScript. Learn prototype-based polymorphism, method overriding for runtime dispatch, duck typing, interface-like patterns, and practical examples of polymorphic design including arrays of mixed types and strategy patterns.
Polymorphism — "many forms" — is the ability to treat different types uniformly by accessing a common interface. In JavaScript, polymorphism is achieved primarily through the prototype chain and duck typing rather than formal interface contracts. Understanding polymorphism helps write code that is extensible, maintainable, and loosely coupled.
The Core Idea: One Interface, Many Behaviors
The simplest polymorphism arises from method overriding in a class hierarchy:
class Shape {
area() { return 0; }
perimeter() { return 0; }
describe() { return `${this.constructor.name}: area=${this.area().toFixed(2)}`; }
}
class Circle extends Shape {
constructor(r) { super(); this.r = r; }
area() { return Math.PI * this.r ** 2; }
perimeter() { return 2 * Math.PI * this.r; }
}
class Rectangle extends Shape {
constructor(w, h) { super(); this.w = w; this.h = h; }
area() { return this.w * this.h; }
perimeter() { return 2 * (this.w + this.h); }
}
class Triangle extends Shape {
constructor(a, b, c) { super(); 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));
}
}
// Polymorphic usage — same code works for all Shape types
const shapes = [
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 4, 5),
];
shapes.forEach(s => console.log(s.describe()));
// "Circle: area=78.54"
// "Rectangle: area=24.00"
// "Triangle: area=6.00"
const totalArea = shapes.reduce((sum, s) => sum + s.area(), 0);
console.log(totalArea.toFixed(2)); // "108.54"The forEach and reduce loops work identically regardless of which specific Shape subclass each object is — this is polymorphism in action.
How JavaScript Enables Polymorphism
JavaScript uses the prototype chain for method dispatch. When shape.area() is called:
- Look for
areaon the instance itself → not found - Look on
instance.__proto__(e.g.,Circle.prototype) → found! Execute it
For an inherited-but-not-overridden method, lookup continues up the chain to Shape.prototype. This runtime dispatch is what makes polymorphism work:
// Method resolution at runtime based on actual type
const s = new Circle(3);
// s.area() resolves: s → Circle.prototype → found area()
// If Circle didn't override area(), it would continue to Shape.prototypeRefer to the JavaScript prototype chain for the full lookup mechanism.
Duck Typing — Structural Polymorphism
JavaScript does not require formal inheritance for polymorphism. If an object has the right method, it works:
// Three objects with no inheritance relationship — just a common .area() method
const circle = {
area() { return Math.PI * 5 ** 2; }
};
const square = {
area() { return 10 ** 2; }
};
const irregularShape = {
area() { return 47.3; } // Computed however is appropriate
};
// Polymorphic function — only requires .area() method
function totalArea(shapes) {
return shapes.reduce((sum, s) => sum + s.area(), 0);
}
console.log(totalArea([circle, square, irregularShape]).toFixed(2)); // "225.84"This is duck typing — "if it walks like a duck and quacks like a duck, treat it as a duck." JavaScript function arguments are duck-typed by default.
Interface-Like Patterns
JavaScript has no interface keyword, but you can approximate interfaces:
// "Interface" via symbol + convention
const Drawable = {
draw: Symbol("draw"),
};
const Resizable = {
resize: Symbol("resize"),
};
class Sprite {
constructor(x, y, width, height) {
this.x = x; this.y = y;
this.width = width; this.height = height;
}
[Drawable.draw](ctx) {
ctx.fillRect(this.x, this.y, this.width, this.height);
}
[Resizable.resize](factor) {
this.width *= factor;
this.height *= factor;
}
}
// Check for interface compliance
function assertDrawable(obj) {
if (typeof obj[Drawable.draw] !== "function") {
throw new TypeError(`${obj.constructor?.name} does not implement Drawable`);
}
}More commonly, interface-like behavior uses runtime method checks:
function render(renderable) {
if (typeof renderable.render !== "function") {
throw new TypeError("Object must have a render() method");
}
renderable.render();
}Method Overriding With super — Extending Behavior
Polymorphism works with super to compose behaviors:
class Logger {
log(event) { console.log(`[LOG] ${JSON.stringify(event)}`); }
}
class MetricsLogger extends Logger {
#metrics = { count: 0, events: [] };
log(event) {
super.log(event); // inherited behavior
this.#metrics.count++;
this.#metrics.events.push(event);
}
getMetrics() { return { ...this.#metrics }; }
}
class TimedLogger extends MetricsLogger {
log(event) {
super.log({ ...event, timestamp: Date.now() }); // extend with timestamp
}
}
const logger = new TimedLogger();
logger.log({ type: "click", target: "button" });
logger.log({ type: "hover", target: "menu" });
console.log(logger.getMetrics().count); // 2For details on super method calls, see using the super keyword in JavaScript classes.
The Strategy Pattern — Polymorphism Without Inheritance
Objects with the same method signature can be swapped at runtime — the strategy pattern:
// Sorting strategies — interchangeable objects with a .sort() method
const ascending = { sort: (arr) => [...arr].sort((a, b) => a - b) };
const descending = { sort: (arr) => [...arr].sort((a, b) => b - a) };
const shuffle = { sort: (arr) => [...arr].sort(() => Math.random() - 0.5) };
class DataList {
constructor(data, strategy = ascending) {
this.data = data;
this.strategy = strategy;
}
getSorted() {
return this.strategy.sort(this.data);
}
setStrategy(strategy) {
this.strategy = strategy;
}
}
const list = new DataList([5, 2, 8, 1, 9, 3]);
console.log(list.getSorted()); // [1, 2, 3, 5, 8, 9]
list.setStrategy(descending);
console.log(list.getSorted()); // [9, 8, 5, 3, 2, 1]This is duck-typed polymorphism — the DataList class does not care what concrete strategy is provided, only that it has a sort method.
Polymorphism vs Overloading
JavaScript does not have function overloading (different behavior based on argument types/count). Common workarounds:
class Vector {
constructor(x, y) { this.x = x; this.y = y; }
// Simulate overloading with type checks
add(other) {
if (other instanceof Vector) {
return new Vector(this.x + other.x, this.y + other.y);
}
if (typeof other === "number") {
return new Vector(this.x + other, this.y + other);
}
throw new TypeError("Expected Vector or number");
}
}
const v1 = new Vector(1, 2);
const v2 = new Vector(3, 4);
console.log(v1.add(v2)); // Vector { x: 4, y: 6 }
console.log(v1.add(5)); // Vector { x: 6, y: 7 }Practical Example: Renderer Polymorphism
A real-world pattern — a rendering system that works with multiple output types:
class HtmlRenderer {
render(component) { return `<div class="${component.type}">${component.content}</div>`; }
}
class MarkdownRenderer {
render(component) {
if (component.type === "heading") return `# ${component.content}`;
return component.content;
}
}
class JsonRenderer {
render(component) { return JSON.stringify(component); }
}
// Polymorphic render function
function renderPage(components, renderer) {
return components.map(c => renderer.render(c)).join("\n");
}
const components = [
{ type: "heading", content: "Hello World" },
{ type: "text", content: "Some paragraph text" },
];
const html = renderPage(components, new HtmlRenderer());
const md = renderPage(components, new MarkdownRenderer());
const json = renderPage(components, new JsonRenderer());
console.log(html);
// <div class="heading">Hello World</div>
// <div class="text">Some paragraph text</div>The renderPage function is fully polymorphic — it works with any renderer that has a .render(component) method.
Rune AI
Key Insights
- Prototype chain is the dispatching mechanism: Method calls follow the prototype chain at runtime to find the most specific implementation — this is what makes override-based polymorphism work
- Duck typing enables structural polymorphism: JavaScript requires no formal inheritance relationship — any object with the right method can be used polymorphically in duck-typed code
- Method overriding is the primary OOP polymorphism tool: Overriding a parent class method in a subclass causes instances of that subclass to use the child version while still fitting anywhere the parent is accepted
- Strategy pattern leverages duck typing: Swappable strategy objects with a common method signature give runtime behavioral polymorphism without inheritance hierarchies
- All JavaScript methods are dynamically dispatched: There are no non-virtual methods — every method call goes through the prototype chain at runtime, making the language inherently polymorphic
Frequently Asked Questions
Is duck typing the same as polymorphism?
When should I use class-based polymorphism vs duck-typed polymorphism?
Does JavaScript have virtual methods?
How does polymorphism interact with private class fields?
Conclusion
Polymorphism in JavaScript is achieved through two complementary mechanisms: prototype-based dispatch (method overriding in class hierarchies) and duck typing (structural compatibility without formal type relationships). The prototype chain enables runtime method resolution — calling shape.area() automatically finds the most specific implementation. Duck typing allows unrelated objects to be used interchangeably as long as they satisfy the expected interface. Together these make JavaScript extremely flexible for polymorphic design, supporting patterns from classic OOP hierarchies to functional strategy patterns and beyond.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.