The idea
What it is
Polymorphism means many forms. The idea: you call one method by name, and each object responds in its own way — based on what it actually is. The caller doesn't need to know, or ask, which kind of object it's holding. It just says "do the thing," and the right behavior runs.
Think of a play button. Press it on a song and you hear audio; press it on a video and you see frames; press it on a podcast and the episode resumes. Same button, same word — play — but the behavior fits whatever media is loaded. Or imagine handing a stack of different shapes to someone and asking each one, "what's your area()?" A circle uses πr², a square uses s², a triangle uses ½bh — and you, the asker, never had to know the formula. Each shape knows its own.
The one sentence to remember
Write the call once against a shared interface; let each object supply its own version of the method. The same line of code, s.area(), runs different behavior depending on the object's real type.
Mechanics
How it works
Runtime polymorphism: overriding and dynamic dispatch
The kind that matters most in object-oriented design is runtime (or subtype) polymorphism. You define a shared base — an interface or abstract class — that declares a method like area(). Then each subtype overrides it with its own implementation. When you call shape.area(), the language looks at the object's actual type at runtime and picks the matching method. That lookup is called dynamic dispatch.
The crucial part: the variable's declared type can be the general one (Shape), but the behavior that runs is the specific one (Circle.area, Square.area). One call site, many possible behaviors, resolved while the program runs.
interface Shape {
area(): number; // the shared promise — every shape can do this
}
class Circle implements Shape {
constructor(private r: number) {}
area() { return Math.PI * this.r ** 2; } // its own formula
}
class Square implements Shape {
constructor(private s: number) {}
area() { return this.s ** 2; } // a different formula
}
const shapes: Shape[] = [new Circle(5), new Square(4)];
for (const s of shapes) {
console.log(s.area()); // SAME call — dynamic dispatch picks the right one
}The killer benefit: delete the type switch
The whole payoff is replacing a sprawling if/switch-on-type with a single polymorphic call. Look at the before: the caller has to know every shape and its formula, and you must edit it every time a new shape appears.
// BEFORE — the caller switches on a type tag (fragile)
function area(shape: any): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.r ** 2;
case "square": return shape.s ** 2;
case "triangle": return 0.5 * shape.b * shape.h;
// add a new shape? you MUST come back and edit this switch.
default: throw new Error("unknown shape");
}
}
// AFTER — one polymorphic call; the caller knows nothing about types
function totalArea(shapes: Shape[]): number {
return shapes.reduce((sum, s) => sum + s.area(), 0); // that's it
}The after version never names a concrete shape. Add a Hexagon that implements Shape, and totalArea keeps working untouched. That's the open/closed principle in one breath: your code is open to new shapes, closed to modification.
The wider landscape (so you know the words)
- Runtime / subtype polymorphism — overriding + dynamic dispatch, the one above. This is what people usually mean in OOP design.
- Compile-time polymorphism (overloading) — several methods share a name but differ by their parameters, e.g.
add(int, int)vsadd(double, double). The compiler picks which one based on the arguments; nothing is decided at runtime. - Parametric polymorphism (generics / templates) — one piece of code works for many types without caring which, e.g.
List<T>orBox<T>. The same logic, parameterized by type.
Dynamic dispatch in one line
When you write s.area(), the method that runs is chosen by s's actual type at runtime — not by the variable's declared type. That single rule is what makes the polymorphic loop possible.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
A row of mixed Shape objects. Click any shape to call shape.area() on it — the dispatch bar shows the same call routing to that shape's own implementation (Circle.area(), Square.area(), …). Flip the toggle to perimeter() and click again — same idea, different method. Or press Run on all for the whole polymorphic loop. The caller never checks the type.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
Click each shape
Click a Circle, then a Square, then a Triangle. The dispatch bar keeps showing the same call shape.area() on the left, but the right side changes — Circle.area(), Square.area(), Triangle.area(). One call site, a different method body each time. That's dynamic dispatch.
Flip the method, click again
Toggle the call from area() to `perimeter()` and click the shapes again. Every shape answers the new call in its own way — 2·π·r, 4·s, 3·s. Same interface, many forms: the whole point of polymorphism.
Add a shape — open/closed
Click + Add shape to drop a new type into the row, then click it (or press Run on all). It just works — the caller has zero if (circle) or switch (kind) to update. New behavior, no caller edits: the open/closed payoff.
In practice
When to use it — and what trips people up
Reach for polymorphism when…
- You catch yourself writing a
switchorif-chain on a type tag (if (kind === "circle")). That's the textbook smell — each branch wants to become an overridden method. - You have a family of things that do the same job differently — shapes with
area(), payment methods withcharge(), exporters withwrite(). - You expect new variants over time and don't want every addition to ripple through caller code.
Where this is heading
Polymorphism is the engine behind whole design patterns. The Strategy pattern swaps interchangeable algorithms behind one interface; the State pattern lets an object change behavior when its internal state changes. Both are polymorphism with a name and a purpose — you'll meet them soon.
What it gives you
- Extensible by default: add a new subtype without touching existing callers (open/closed).
- Deletes type switches — no
if/switch-on-kind scattered across the codebase. - Callers stay clean and general: they program to the interface, not to concrete types.
- Each behavior lives next to the data it needs, so related logic is in one place.
Common mistakes
- Indirection can hide control flow — "jump to definition" lands on an interface, not the code that actually runs.
- Overusing it: a tiny one-off conditional doesn't need a class hierarchy and three new files.
- LSP violations — an override that does something surprising (throws, no-ops, changes the contract) breaks callers that trusted the interface.
- Too many thin subtypes can fragment logic, making the whole behavior hard to see at a glance.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
// One interface, many implementations. Pick your language above.
interface Shape {
area(): number;
}
class Circle implements Shape {
constructor(private r: number) {}
area(): number { return Math.PI * this.r ** 2; }
}
class Square implements Shape {
constructor(private s: number) {}
area(): number { return this.s ** 2; }
}
// No type checks: one polymorphic call for every shape.
function totalArea(shapes: Shape[]): number {
return shapes.reduce((sum, s) => sum + s.area(), 0);
}
const shapes: Shape[] = [new Circle(5), new Square(4)];
console.log(totalArea(shapes)); // 78.54 + 16 = 94.54References & further reading
5 sources- Docsdocs.oracle.com
Oracle — Polymorphism
The classic Java tutorial showing overriding and dynamic dispatch with a clean example.
- Docsdeveloper.mozilla.org
MDN — extends
How subclassing and method overriding work in JavaScript, with runnable examples.
- Articlerefactoring.guru
Refactoring.Guru — Replace Conditional with Polymorphism
A step-by-step refactoring that turns a type switch into overridden methods — the killer benefit in practice.
- Articleen.wikipedia.org
Wikipedia — Polymorphism (computer science)
A map of the whole landscape: subtype, parametric, and ad-hoc (overloading) polymorphism.
- Articleen.wikipedia.org
Wikipedia — Dynamic dispatch
The mechanism that picks the right method at runtime — the engine under subtype polymorphism.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Nothing is scored or saved.
question 01 / 04
In a loop that calls s.area() on a list of mixed Shape objects, what decides which area() actually runs?
question 02 / 04
What is the main advantage of replacing a switch-on-type with a polymorphic method call?
question 03 / 04
Which of these is compile-time (ad-hoc) polymorphism rather than runtime polymorphism?
question 04 / 04
A new Triangle subtype overrides area() but secretly returns 0 and logs a warning instead of the real area. Why is that a problem?
0/4 answered