Polymorphism
Allow different types to be used interchangeably through a shared interface — so the calling code doesn't need to know which concrete type it's working with.
★★★★★4/5Inside a codebase — classes, modules, files
How it works
Polymorphism (Greek: 'many forms') lets a single piece of code work with many different types, as long as they implement the expected interface. The two main forms in OOP are:
**Subtype polymorphism (runtime)**: a variable typed as an interface or base class can hold any conforming implementation. The correct method is dispatched at runtime based on the actual type.
**Parametric polymorphism (compile-time / generics)**: a function is written once and works with any type that satisfies a constraint (TypeScript generics, Rust traits, Go type parameters).
Polymorphism is what makes Strategy, Observer, and Dependency Injection work. A PaymentProcessor variable can hold a StripeProcessor or PayPalProcessor — the checkout function doesn't care which. At runtime, the right charge() implementation is called. Adding a new payment method means adding a new class, not changing the checkout code.
Implementation
TypeScript · Go · Rustinterface PaymentProcessor {
charge(amount: number, currency: string): Promise<string>;
}
class StripeProcessor implements PaymentProcessor {
async charge(amount: number, currency: string): Promise<string> {
// Stripe API call
return `stripe_txn_${Date.now()}`;
}
}
class PayPalProcessor implements PaymentProcessor {
async charge(amount: number, currency: string): Promise<string> {
// PayPal API call
return `paypal_txn_${Date.now()}`;
}
}
// Adding a new processor = adding a new class, not changing checkout
class CryptoProcessor implements PaymentProcessor {
async charge(amount: number, currency: string): Promise<string> {
return `crypto_txn_${Date.now()}`;
}
}
// Same code path regardless of which processor is injected
async function checkout(processor: PaymentProcessor, total: number): Promise<string> {
return processor.charge(total, "USD");
}Why it matters
Code that uses concrete types directly must change every time a new variant is added. Code that uses a polymorphic interface stays unchanged — only the implementation list grows. Polymorphism is the runtime expression of the Open/Closed Principle.
✓ When to use
- →Multiple interchangeable implementations of a concept (renderers, payment processors, loggers)
- →When new variants must be addable without changing existing code
- →Eliminating type-switch or if/else chains conditioned on type
- →Plugin architectures and extensibility points
✗ When NOT to use
- →When there is genuinely only one implementation and no extension is planned
- →Simple data containers with no behaviour don't benefit from polymorphic interfaces
Trade-offs
New implementations can be added without changing calling code
Dynamic dispatch has a small runtime cost vs direct method calls
Eliminates type-checking conditionals and switch statements
Too many small interfaces add indirection without clarity
Enables substitution — the basis of testability via test doubles
Abuse leads to deep hierarchies that are hard to follow
In production
List<T> is polymorphic — ArrayList and LinkedList are interchangeable; code written to List works with either
Everything that can be read implements io.Reader — files, network connections, in-memory buffers, all interchangeable
react-dom, react-native, react-three-fiber all implement the same React component interface — same components render everywhere
Industry adoption
Related principles
Interface / Contract
Define what a component must do without dictating how it does it — so implementations can vary freely while callers remain stable.
Abstraction
Reduce complexity by modelling only the details that matter for your problem — hide everything else behind a stable interface.
Strategy Pattern
Define a family of interchangeable algorithms behind a common interface so the calling context can swap them at runtime without changing its own code.
Open/Closed Principle
Software entities should be open for extension but closed for modification — add new behaviour by writing new code, not by changing existing code.