Overview
Polymorphism means 'many forms': the same call behaves differently depending on the actual object. When code calls shape.area(), a Circle and a Rectangle each run their own implementation. The caller works with the common type and never checks which concrete type it has.
Runtime (subtype) polymorphism comes from interfaces and overriding and is the one that matters most in design: it replaces switch-on-type code, supports the Open-Closed Principle, and powers nearly every design pattern. Compile-time polymorphism includes method overloading and generics.
Pressing play on a music app, a video player, or a game console all 'play', but each does something different. You do not need to know the device's internals to press play.
When to use it
- Code switches on a type or enum to decide behavior.
- New variants will be added over time.
- Collections of different but related objects processed uniformly.
Where it shows up in interviews
Recognize it when: switch(type) repeated in several places.
- Design a payment system
- Design a notification service
Recognize it when: iterate over mixed items (shapes, vehicles, employees).
- Design a drawing app
- Design payroll for employee types
Where it is used in real software
Code written against List works with ArrayList, LinkedList, or immutable lists.
IDE and browser extensions implement shared interfaces the host calls polymorphically.
Express middleware and Spring filters are all called the same way but do different work.
Key terms
- Subtype polymorphism
- One interface, many implementations, chosen at runtime.
- Dynamic dispatch
- The runtime picks the method based on the actual object.
- Overloading
- Same name, different parameter lists (compile time).
- Parametric polymorphism
- Generics: List<T> works for any T.
How it works, step by step
- 1Spot the type switch
if (type === 'circle') ... else if ...
- 2Define the common operation
area(): number.
- 3Move each branch into its type
Circle.area(), Rectangle.area().
- 4Call through the common type
shapes.map(s => s.area()).
- 5Add new types without editing callers
Triangle implements Shape.
Switch vs polymorphism
Computing pay for full-time, contractor, and intern
| Concern | switch(employee.type) | Polymorphic pay() |
|---|---|---|
| Add a new type | Edit every switch | Add one class |
| Forgotten branch | Silent bug or default | Compile error: must implement pay() |
| Testing | Test giant method | Test each class |
NOWConcern: Add a new type | switch(employee.type): Edit every switch | Polymorphic pay(): Add one class
Polymorphism moves the decision into the type system; the switch disappears from business code.
Implementation
interface Employee { name: string; monthlyPay(): number;} class FullTime implements Employee { constructor(readonly name: string, private salary: number) {} monthlyPay() { return this.salary / 12; }} class Contractor implements Employee { constructor(readonly name: string, private hourly: number, private hours: number) {} monthlyPay() { return this.hourly * this.hours; }} class Intern implements Employee { constructor(readonly name: string, private stipend: number) {} monthlyPay() { return this.stipend; }} const staff: Employee[] = [new FullTime("Ana", 120_000), new Contractor("Bo", 80, 120), new Intern("Cy", 2_000)];const payroll = staff.reduce((sum, e) => sum + e.monthlyPay(), 0); // no type checksComplexity and performance
Indirect call.
No caller changes.
Trade-offs
Polymorphism makes new types easy but new operations require changing every class; the Visitor pattern flips this.
Behavior is spread across classes; an IDE is needed to find all implementations.
Variants and related techniques
Java 21 switch and TypeScript unions give exhaustive checks when operations change more than types.
Python and JavaScript dispatch on whatever methods an object has.
Common mistakes
- instanceof checks after introducing an interface.
Fix: Move the behavior into the interface.
- Overloading confused with overriding.
Fix: Overloads are chosen at compile time by static type, not the runtime object.
Interview questions
How does polymorphism support the Open-Closed Principle?
Callers depend on an interface, so new behavior is added by writing a new implementation rather than modifying existing callers.
Overloading vs overriding?
Overloading: same method name with different parameters, resolved at compile time. Overriding: a subclass replaces a parent method, resolved at runtime by the actual object.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Replace a notification-type switch with polymorphism | Easy | Refactoring. |
| Design shapes with area and perimeter | Easy | Interfaces. |