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
- The implementation signature is NOT callable from outside. Only the overloads are.
- The implementation signature must be a superset of all overloads (its params/return must satisfy every overload).
- 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)vsslice(start, end)). - Mutually exclusive options (e.g., either pass
idORname, 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.