OOP Interview Prep
Polymorphism

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


Overloading vs Overriding thumbnail


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)

Overloading vs Overriding visual 1


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 super when partial extension is the intent. When a child class overrides a method but should also run the parent's version, calling super.methodName() is required. Omitting it means the parent's logic is silently dropped. The PushNotification example above shows the correct pattern.

  • Using override keyword inconsistently. In TypeScript, if you enable noImplicitOverride in tsconfig.json, any method that overrides a parent method without the override keyword 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
---------------------------------------------------------------------------

Overloading vs Overriding visual 2


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.

On this page