Function Overloading

intermediate functions overloading

In simple language, function overloading lets one function accept several different argument patterns, each with its own return type. TypeScript only does this at the type level — at runtime there’s still just one function.

The pattern: write 2+ “overload signatures” with no body, then ONE “implementation signature” with the body. Callers only see the overloads; the implementation is hidden.

// overload signatures (visible to callers)
function parse(input: string): object;
function parse(input: number): string;

// implementation signature (hidden, must be compatible with all overloads)
function parse(input: string | number): object | string {
  if (typeof input === "string") return JSON.parse(input);
  return input.toString();
}

const a = parse('{"x":1}'); // a: object
const b = parse(42);        // b: string

Why not just use unions?

Good question — and often a union is enough. The difference shows up when the return type DEPENDS on the input type.

// union version — return type is always the same
function bad(x: string | number): string | number {
  return x; // caller always gets string | number
}

// overload version — return narrows based on input
function good(x: string): string;
function good(x: number): number;
function good(x: string | number) { return x; }

const s = good("hi"); // s: string  ← narrowed!
const n = good(42);   // n: number

Rules to remember

  1. The implementation signature is NOT callable from outside. Only the overloads are.
  2. The implementation signature must be a superset of all overloads (its params/return must satisfy every overload).
  3. Order matters — TypeScript picks the FIRST matching overload, so put the most specific one first.
function fmt(x: boolean): "yes" | "no";
function fmt(x: number): string;
function fmt(x: boolean | number): string {
  return typeof x === "boolean" ? (x ? "yes" : "no") : x.toFixed(2);
}

When to prefer overloading

  • The return type changes based on input shape.
  • Different parameter counts (e.g., slice(start) vs slice(start, end)).
  • Mutually exclusive options (e.g., either pass id OR name, never both).

When to AVOID overloading

If a union or generic does the job, use that. Overloads are duplicate-y and easy to get out of sync. Modern advice: try function f<T extends ...> or conditional types first.

// often cleaner than overloads:
function pick<T extends "yes" | "no" | number>(x: T): T {
  return x;
}

Common interview prompt

“Write a function combine that returns string when both args are strings and number when both are numbers.” That’s a textbook overload — or a generic with conditional types, depending on how fancy we want to be.