Overloading vs Overriding
The Polymorphism Pair Every Interviewer Tests
LinkedIn Hook
Here is the question that ends interviews early for a lot of mid-level candidates:
"What is the difference between method overloading and method overriding?"
Most people can give a rough definition. Very few can follow up cleanly when pressed: "Which one is compile-time polymorphism? Which one requires a parent-child relationship? Why doesn't JavaScript support true overloading natively?"
Knowing both definitions in isolation is not enough. Interviewers want to hear you compare them, contrast them, and explain when each one applies. That distinction is what this lesson is built around.
Read the full lesson with code and comparison table → [link]
#OOP #TypeScript #JavaScript #Polymorphism #InterviewPrep
What You'll Learn
- The precise definition of method overloading and method overriding, and how they differ at every level
- A comprehensive side-by-side comparison table covering class relationship, polymorphism type, binding time, language support, and use cases
- Why JavaScript does not support native method overloading and how to simulate it in practice
- The three most common interview traps built around this topic
- Code examples in both JavaScript and TypeScript showing each concept cleanly
The Analogy That Makes It Click
Think about a customer service rep at a bank.
Overloading is like a single rep who handles different request types depending on what the customer brings. You hand her a form, she processes a form. You call her with an account number, she looks up an account. You walk in with a complaint, she opens a ticket. Same person, same job title, different inputs, different handling. Everything happens inside one department, one "class."
Overriding is different. Imagine a regional branch that inherits its procedures from corporate HQ. HQ has a standard procedure for handling loan applications. But the regional branch has a local regulation that requires an extra verification step, so they replace the corporate procedure with their own version. Same procedure name, same signature, but the child branch's version runs instead of the parent's when you visit that branch.
Overloading: one class, same name, different inputs. Overriding: parent-child relationship, same name and signature, child replaces parent behavior.
[INTERNAL-LINK: how overriding works with super → Lesson 5.2: Method Overriding]
What Is Method Overloading?
Method overloading means defining multiple versions of the same method in the same class, each accepting a different set of parameters. The correct version is selected based on the arguments passed at the call site. This selection happens at compile time, which is why overloading is classified as compile-time polymorphism (also called static polymorphism or early binding).
Java and C++ support native overloading. TypeScript supports it through declaration signatures combined with a single implementation body. JavaScript has no native overloading at all since it has no compile-time type system. In JavaScript, you simulate it using argument inspection inside the function body.
[INTERNAL-LINK: JavaScript overloading simulation patterns → Lesson 5.3: Method Overloading]
What Is Method Overriding?
Method overriding means a child class provides its own implementation for a method that already exists in a parent class. The method must have the same name, the same parameter types, and the same return type as the parent's version. The child's version replaces the parent's version at runtime when the object is an instance of the child class.
Overriding is classified as runtime polymorphism (also called dynamic polymorphism or late binding). The decision about which version to run is made at runtime based on the actual type of the object, not the declared type. This is the mechanism that powers the classic shape.draw() example where Circle, Square, and Triangle all respond differently to the same method call.
[INTERNAL-LINK: runtime dispatch and the virtual method table concept → Lesson 5.1: Polymorphism]
Comparison Table — Overloading vs Overriding
OVERLOADING vs OVERRIDING — FULL COMPARISON
---------------------------------------------------------------------------
Feature Method Overloading Method Overriding
---------------------------------------------------------------------------
Definition Same method name, Same method name and
different parameter signature in a child
list, same class class replaces parent's
implementation
Class relationship Same class (or Requires inheritance
required TypeScript declarations (parent-child)
in same class)
Polymorphism type Compile-time Runtime
(static / early binding) (dynamic / late binding)
When decision At compile time — At runtime — based on
is made based on argument the actual object type
types and count
Method signature Must DIFFER (parameters Must be IDENTICAL
differ in type, count, (same name, same params,
or order) same return type)
Return type Can differ in Java Must be same (or
(not in TypeScript) covariant in Java)
Access modifier Can change freely Cannot be more
rules restrictive than parent
`super` keyword Not applicable Used to call the parent
class version
Native JavaScript No — must simulate Yes — works natively
support via argument checking via prototype chain
TypeScript support Yes — via overload Yes — fully supported
signatures + one with `override` keyword
implementation (TS 4.3+)
`override` keyword Not applicable `override` keyword in
(TypeScript) TypeScript marks intent
and catches errors
Inheritance Not required Required — child must
required? extend parent
Use case One method handles Child class customizes
multiple input shapes or extends parent
(e.g., add(int, int) behavior for its
vs add(float, float)) specific needs
Example calculate(10, 5) circle.area() calls
vs calculate(10, 5, 2) Circle's formula,
select different not Shape's placeholder
behavior by arg count
---------------------------------------------------------------------------
[IMAGE: Side-by-side diagram showing overloading (one class, multiple method signatures) vs overriding (parent-child class pair, matching method signature) - search terms: "method overloading overriding diagram OOP"]
Code Example 1 — Method Overloading in TypeScript
TypeScript's overloading system requires you to write declaration signatures above a single unified implementation. The signatures define what callers can pass. The implementation handles all cases internally.
class Calculator {
// Overload signatures — these define what callers can use
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: number, b: number, c: number): number;
// Single implementation body — handles all overload cases
add(a: number | string, b: number | string, c?: number): number | string {
// Case 1: three numbers
if (typeof a === "number" && typeof b === "number" && c !== undefined) {
return a + b + c;
}
// Case 2: two strings
if (typeof a === "string" && typeof b === "string") {
return a + " " + b;
}
// Case 3: two numbers
if (typeof a === "number" && typeof b === "number") {
return a + b;
}
throw new Error("Invalid argument combination");
}
}
const calc = new Calculator();
console.log(calc.add(10, 5)); // Output: 15
console.log(calc.add(10, 5, 3)); // Output: 18
console.log(calc.add("Hello", "World")); // Output: Hello World
// TypeScript enforces the declared signatures at compile time
// calc.add(10, "World"); // Compile error — no overload matches this call
The key point: TypeScript resolves the correct signature at compile time using the type information. At runtime in JavaScript, there is only one function. The overloading is a compile-time construct layered on top of a single runtime function.
Code Example 2 — Simulating Overloading in Plain JavaScript
JavaScript has no type system at compile time, so there is no mechanism to declare multiple signatures. You simulate overloading by inspecting arguments inside the function body.
class TextFormatter {
// JavaScript does not support multiple function declarations with the same name
// The last definition would overwrite all previous ones
// Instead: inspect arguments at runtime to simulate overloading
format(input, separator, repeat) {
// Simulation 1: called with one argument — wrap in brackets
if (arguments.length === 1) {
return `[${input}]`;
}
// Simulation 2: called with two arguments — join with separator
if (arguments.length === 2 && typeof separator === "string") {
return input.split("").join(separator);
}
// Simulation 3: called with three arguments — repeat the string
if (arguments.length === 3 && typeof repeat === "number") {
return (input + separator).repeat(repeat).trim();
}
throw new Error("Unsupported argument combination");
}
}
const formatter = new TextFormatter();
console.log(formatter.format("hello")); // Output: [hello]
console.log(formatter.format("hello", "-")); // Output: h-e-l-l-o
console.log(formatter.format("ha", "!", 3)); // Output: ha!ha!ha
// Common alternative: use an options object to unify the signature
class QueryBuilder {
select(options) {
// options can be: string, array of strings, or an object with filters
if (typeof options === "string") {
return `SELECT ${options} FROM table`;
}
if (Array.isArray(options)) {
return `SELECT ${options.join(", ")} FROM table`;
}
if (typeof options === "object" && options.fields) {
const fields = options.fields.join(", ");
const where = options.where ? ` WHERE ${options.where}` : "";
return `SELECT ${fields} FROM table${where}`;
}
throw new Error("Invalid select options");
}
}
const qb = new QueryBuilder();
console.log(qb.select("*"));
// Output: SELECT * FROM table
console.log(qb.select(["id", "name", "email"]));
// Output: SELECT id, name, email FROM table
console.log(qb.select({ fields: ["id", "name"], where: "age > 18" }));
// Output: SELECT id, name FROM table WHERE age > 18
[PERSONAL EXPERIENCE]: In practice, the options object pattern (last example above) is far cleaner than inspecting arguments.length. It scales better when the number of variations grows, and it makes call sites self-documenting. Argument-count checking becomes brittle the moment a third developer adds a new variation without reading the existing cases.
Code Example 3 — Method Overriding in TypeScript
Overriding requires a parent-child class relationship. The child provides its own version of a method defined in the parent. TypeScript's override keyword (introduced in version 4.3) explicitly marks the intent and catches mistakes at compile time.
// Parent class — defines the base behavior
class Notification {
protected recipient: string;
protected message: string;
constructor(recipient: string, message: string) {
this.recipient = recipient;
this.message = message;
}
// Base implementation — child classes will override this
send(): void {
console.log(`Sending notification to ${this.recipient}: ${this.message}`);
}
// Base implementation — child classes may override this
formatMessage(): string {
return `[NOTIFICATION] ${this.message}`;
}
}
// Child class — overrides send() with email-specific behavior
class EmailNotification extends Notification {
private subject: string;
constructor(recipient: string, message: string, subject: string) {
super(recipient, message);
this.subject = subject;
}
// `override` keyword signals intent and enables compiler checks
override send(): void {
console.log(`Sending EMAIL to ${this.recipient}`);
console.log(`Subject: ${this.subject}`);
console.log(`Body: ${this.formatMessage()}`);
}
override formatMessage(): string {
return `<p>${this.message}</p>`;
}
}
// Another child class — overrides with SMS-specific behavior
class SMSNotification extends Notification {
private phoneNumber: string;
constructor(recipient: string, message: string, phoneNumber: string) {
super(recipient, message);
this.phoneNumber = phoneNumber;
}
override send(): void {
console.log(`Sending SMS to ${this.phoneNumber}`);
console.log(`Text: ${this.formatMessage()}`);
}
override formatMessage(): string {
// SMS messages are typically short — strip formatting
return this.message.substring(0, 160);
}
}
// Child class that calls super — extends rather than fully replaces
class PushNotification extends Notification {
private deviceToken: string;
constructor(recipient: string, message: string, deviceToken: string) {
super(recipient, message);
this.deviceToken = deviceToken;
}
override send(): void {
// Call parent behavior first, then add push-specific logic
super.send();
console.log(`Push delivered to device: ${this.deviceToken}`);
}
}
// Runtime polymorphism: the same send() call dispatches to different implementations
// based on the actual object type at runtime, not the declared type
const notifications: Notification[] = [
new EmailNotification("alice@example.com", "Your order shipped.", "Order Update"),
new SMSNotification("Bob", "Your code is 847291", "+1-555-0100"),
new PushNotification("Carol", "New message from Dave", "device-token-xyz"),
];
notifications.forEach((n) => n.send());
// Output:
// Sending EMAIL to alice@example.com
// Subject: Order Update
// Body: <p>Your order shipped.</p>
//
// Sending SMS to +1-555-0100
// Text: Your code is 847291
//
// Sending notification to Carol: New message from Dave
// Push delivered to device: device-token-xyz
Notice the forEach loop. Every object in the array is typed as Notification, but each send() call runs the child class version. The runtime checks the actual object type and dispatches accordingly. This is runtime polymorphism in action.
[UNIQUE INSIGHT]: The override keyword in TypeScript is not required for overriding to work — it has worked without the keyword since TypeScript's early versions. Its value is purely defensive: if you rename the parent method later, any child method marked override that no longer matches will throw a compile error. Without override, the mismatch silently creates a new, unrelated method on the child class. That silent failure is one of the most common sources of subtle bugs in large TypeScript codebases.
Code Example 4 — The Interview Trap: Overloading and Overriding Together
A common advanced interview question asks you to demonstrate both concepts working in the same codebase. This example shows them clearly separated.
// Base class — will be used for overriding
class Shape {
protected color: string;
constructor(color: string) {
this.color = color;
}
// This method will be overridden by child classes
area(): number {
return 0;
}
describe(): string {
return `A ${this.color} shape with area: ${this.area()}`;
}
}
// Child class — demonstrates OVERRIDING (runtime polymorphism)
class Rectangle extends Shape {
private width: number;
private height: number;
constructor(color: string, width: number, height: number) {
super(color);
this.width = width;
this.height = height;
}
// OVERRIDING — replaces Shape's area() at runtime
override area(): number {
return this.width * this.height;
}
// OVERLOADING within the same class (TypeScript signatures)
// Same method name, different parameter shapes
scale(factor: number): Rectangle;
scale(widthFactor: number, heightFactor: number): Rectangle;
scale(widthFactor: number, heightFactor?: number): Rectangle {
if (heightFactor === undefined) {
// One argument: scale both dimensions equally
return new Rectangle(this.color, this.width * widthFactor, this.height * widthFactor);
}
// Two arguments: scale each dimension independently
return new Rectangle(this.color, this.width * widthFactor, this.height * heightFactor);
}
}
const rect = new Rectangle("blue", 5, 3);
console.log(rect.area()); // Output: 15 (overriding — child version runs)
console.log(rect.describe()); // Output: A blue shape with area: 15
const scaled1 = rect.scale(2); // Overloading — one-argument version
const scaled2 = rect.scale(2, 3); // Overloading — two-argument version
console.log(scaled1.area()); // Output: 60 (10 * 6)
console.log(scaled2.area()); // Output: 45 (10 * 9)
// Runtime polymorphism in action
const shapes: Shape[] = [
new Shape("gray"),
new Rectangle("red", 4, 6),
];
shapes.forEach(s => console.log(s.describe()));
// Output:
// A gray shape with area: 0 (Shape's own area() runs)
// A red shape with area: 24 (Rectangle's overridden area() runs)
Common Mistakes
-
Confusing the binding time. Overloading is resolved at compile time by the type checker. Overriding is resolved at runtime by the actual object type. Mixing these up in an interview is the fastest way to signal a shallow understanding of polymorphism. Practice saying it clearly: "overloading is early binding, overriding is late binding."
-
Thinking JavaScript supports native overloading. It does not. If you define two functions with the same name in the same scope, the second silently overwrites the first. JavaScript has no compile-time type system to differentiate signatures. TypeScript adds overload declarations, but these are erased at compile time. The runtime always sees one function.
-
Changing the method signature when trying to override. If a child class method has the same name as a parent method but different parameters, it is not an override. In JavaScript, it silently replaces the parent method regardless. In TypeScript with
override, the compiler flags the mismatch. In Java, the compiler treats it as overloading, not overriding, which causes extremely confusing behavior when the base type is used for the reference. -
Forgetting
superwhen partial extension is the intent. When a child class overrides a method but should also run the parent's version, callingsuper.methodName()is required. Omitting it means the parent's logic is silently dropped. ThePushNotificationexample above shows the correct pattern. -
Using
overridekeyword inconsistently. In TypeScript, if you enablenoImplicitOverrideintsconfig.json, any method that overrides a parent method without theoverridekeyword becomes a compile error. Enabling this option is strongly recommended. It makes all overriding decisions explicit and prevents silent method hiding.
Interview Questions
Q: What is the fundamental difference between method overloading and method overriding?
Overloading defines multiple methods with the same name but different parameter lists in the same class. The correct version is selected at compile time based on argument types. Overriding defines a method in a child class with the same name and signature as a parent method. The correct version is selected at runtime based on the object's actual type. Overloading is compile-time (static) polymorphism. Overriding is runtime (dynamic) polymorphism.
Q: Does JavaScript support method overloading? How do you handle it?
JavaScript has no native method overloading. There is no compile-time type system to distinguish signatures. If you write two functions with the same name, the second overwrites the first. In practice, you simulate overloading by inspecting argument count or types inside a single function body, or by accepting an options object that handles all variations. TypeScript adds overload declaration signatures on top of a single implementation, which provides compile-time safety without changing the runtime behavior.
Q: Can you override a method and also call the parent version?
Yes. Inside the overriding method, super.methodName() calls the parent class version. This is useful when the child needs to extend rather than completely replace the parent's behavior. You call super.send() first to run shared logic, then add the child-specific steps. If the parent version is not needed at all, you simply omit the super call and the child's implementation runs alone.
Q: What happens in TypeScript if you change a parameter type when overriding?
If you declare the same method name in a child class with different parameter types, TypeScript treats it as a new method on the child, not an override of the parent. With override keyword enabled, the compiler flags the mismatch immediately. Without the keyword, the parent version is hidden on the prototype chain when called through the child reference, but it remains accessible through the parent reference. This silent method hiding is a common source of hard-to-trace bugs, which is why noImplicitOverride is recommended in production TypeScript projects.
Q: If you have an array typed as Animal[] containing Dog and Cat objects, and you call speak() on each, which method runs?
The child class version runs every time. TypeScript's declared type is Animal, but the actual runtime objects are Dog and Cat. JavaScript's prototype chain resolves speak() by looking at the actual object's prototype first. If Dog has its own speak(), that version runs. If Cat has its own speak(), that version runs. The declared type only matters at compile time for type checking. Runtime dispatch always follows the actual object's prototype chain. This is the core mechanism of runtime polymorphism.
Quick Reference Cheat Sheet
OVERLOADING vs OVERRIDING — QUICK REFERENCE
---------------------------------------------------------------------------
OVERLOADING OVERRIDING
---------------------------------------------------------------------------
Also called Static polymorphism Dynamic polymorphism
Compile-time binding Runtime binding
Early binding Late binding
Requires Same class Parent-child (extends)
inheritance? No Yes
Method signature Must differ Must be identical
(different params) (same name + params)
When resolved Compile time Runtime
Decision basis Argument types Actual object type
and count at runtime
Return type Can vary (Java) Must match parent
(or be covariant)
`super` usage Not applicable super.method() calls
parent version
Native JS support No — simulate Yes — prototype chain
via arg inspection handles it natively
TypeScript support Yes — overload Yes — `override`
declaration keyword (TS 4.3+)
signatures
tsconfig option N/A noImplicitOverride: true
Key intent One method, many Child replaces or
input shapes extends parent behavior
---------------------------------------------------------------------------
OVERLOADING PATTERNS IN JAVASCRIPT
---------------------------------------------------------------------------
Pattern 1: Argument count check
function process(a, b, c) {
if (arguments.length === 1) { ... }
if (arguments.length === 2) { ... }
if (arguments.length === 3) { ... }
}
Pattern 2: Type check
function handle(input) {
if (typeof input === "string") { ... }
if (Array.isArray(input)) { ... }
if (typeof input === "object") { ... }
}
Pattern 3: Options object (recommended)
function query({ fields, where, limit } = {}) {
// single signature, flexible behavior
}
TYPESCRIPT OVERLOAD SYNTAX
---------------------------------------------------------------------------
// Declaration signatures (what callers see)
method(a: number): number;
method(a: string): string;
// Implementation signature (handles all cases)
method(a: number | string): number | string {
if (typeof a === "number") return a * 2;
return a.toUpperCase();
}
OVERRIDING WITH TYPESCRIPT override KEYWORD
---------------------------------------------------------------------------
class Child extends Parent {
override methodName(): void { // compiler verifies parent has this
super.methodName(); // optional: call parent version
// child-specific logic
}
}
ENABLE STRICT OVERRIDE CHECKING (tsconfig.json)
---------------------------------------------------------------------------
{
"compilerOptions": {
"noImplicitOverride": true // any override must use `override`
}
}
SIDE-BY-SIDE MENTAL MODEL
---------------------------------------------------------------------------
OVERLOADING:
class Printer {
print(text: string): void { ... } // version 1
print(text: string, copies: number): void { ... } // version 2
} ^--- same class, different params
OVERRIDING:
class Printer {
print(): void { console.log("generic print"); }
}
class LaserPrinter extends Printer {
override print(): void { console.log("laser print"); }
} ^--- child class, same signature, new behavior
---------------------------------------------------------------------------
Previous: Lesson 5.4 - Constructor Overloading Next: Lesson 6.1 - Association
This is Lesson 5.5 of the OOP Interview Prep Course — 8 chapters, 41 lessons.