Overloading vs Overriding in TypeScript

What’s the difference between overloading and overriding in TypeScript? Learn how they work and when to use them effectively in your code.

Introduction

If you’re writing object-oriented TypeScript, you’ve likely come across two terms that sound similar but play very different roles: overloading and overriding. Understanding the difference between them is key to writing robust, scalable TypeScript code.

Let’s break each concept down clearly, with real-world examples, and explore how they relate, especially in the context of the Liskov Substitution Principle, a key part of the SOLID design principles.

Function Overloading in TypeScript

Function overloading in TypeScript allows you to define multiple function signatures with different parameter types or counts, while still providing a single implementation.

This is especially useful for functions that need to handle various input types gracefully.

function format(input: number): string;
function format(input: string, suffix: string): string;

function format(input: number | string, suffix?: string): string {
  return typeof input === "number"
    ? `$${input.toFixed(2)}`
    : input.trim() + `_${suffix}`;
}

format(42);         // "$42.00"
format(" hello ", "world");  // "hello_world"

As you see, the function has two different signatures with different parameter counts and types, but has a single implementation.

You’re giving developers the impression of multiple versions of a function, but under the hood, TypeScript only emits one function implementation. The rest is just type metadata for the compiler.

Quick Recap:

  • Overloading is a TypeScript-only feature.
  • You define multiple signatures, but write one actual implementation.
  • It just improves the developer experience, not runtime behavior.

Method Overriding in TypeScript

Now let’s talk about overriding. This happens in a class-based inheritance setup. A subclass can redefine a method it inherits from a parent class to change or specialize its behavior.

Example:

class Vehicle {
  move(): string {
    return "Moving forward";
  }
}

class Car extends Vehicle {
  move(): string {
    return "Driving on the road";
  }
}

Here, the Car class overrides the move method from Vehicle. When you call car.move(), it uses the version defined in Car, not the one in Vehicle.

Key Differences:

  • Overriding is part of runtime behavior.
  • It exists in both TypeScript and JavaScript.
  • It’s how you implement polymorphism.

Liskov Substitution Principle (LSP) & Overriding

When overriding, you need to be careful not to break expectations. That’s where the Liskov Substitution Principle comes in.

LSP says: any subclass should be replaceable for its parent class without breaking the application.

Bad Example (Violates LSP):

class Bird {
  fly(): string {
    return "Flying!";
  }
}

class Penguin extends Bird {
  fly(): string {
    throw new Error("Penguins can't fly");
  }
}

Now, imagine code that expects any Bird to be able to fly. Passing a Penguin breaks that assumption—and violates LSP.

A better approach would be to redesign the class hierarchy:

class Bird {}

class FlyingBird extends Bird {
  fly(): string {
    return "Flying!";
  }
}

class Penguin extends Bird {
  // No fly() method here — because it can't fly
}

Following LSP helps you keep polymorphism safe and maintainable.

Overloading vs Overriding: What’s the Real Difference?

Let’s compare them briefly:

  • Overloading is compile-time only. It’s for when a function needs to support multiple signatures in the same scope. It only exists in TypeScript.
  • Overriding is about runtime behavior. It lets subclasses replace inherited methods and is available in both TypeScript and JavaScript.

They’re not interchangeable—each serves a different role in your design.

Are These Features in JavaScript?

Function Overloading in JS?

JavaScript doesn’t support function overloading. If you try defining multiple functions with the same name, only the last one survives:

function show(data) {
  console.log("First");
}

function show(data, type) {
  console.log("Second");
}

show("test"); // "Second"

This is why TypeScript’s overloading system is purely syntactic—it vanishes at compile time.

Method Overriding in JS?

JavaScript does support method overriding through prototypal inheritance:

class Animal {
  speak() {
    return "Generic sound";
  }
}

class Cat extends Animal {
  speak() {
    return "Meow";
  }
}

This works exactly as you’d expect—it calls the method closest to the object in the prototype chain.

Conclusion

Overloading and overriding are two essential tools in the TypeScript toolbox, and understanding the difference is crucial if you’re building scalable, maintainable systems.

  • Overloading gives you flexibility at compile time.
  • Overriding gives you control at runtime.
  • And the Liskov Substitution Principle reminds you not to override recklessly.

So, when to use each:

  • Use overloading when a single function should behave differently based on input types or arguments. Perfect for utilities and SDKs.
  • Use overriding when you’re dealing with class hierarchies and want to customize behavior in a subclass, but don’t break the contract set by the base class.

Know when to use each, and your TypeScript code will be solid, safe, and way easier to reason about.

Think Of It

If you enjoyed this article, I’d truly appreciate it if you could share it—it really motivates me to keep creating more helpful content!

If you’re interested in exploring more, check out these articles.

Thanks for sticking with me until the end—I hope you found this article valuable and enjoyable!

Want more dev insights like this? Subscribe to get practical tips, tutorials, and tech deep dives delivered to your inbox. No spam, unsubscribe anytime.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top