Function overloads
How overloads expose multiple call signatures over one hidden implementation, how the compiler resolves a call top-to-bottom and picks the first match, and when a single generic or conditional return beats overloads.
A teammate ships a createElement(tag) helper. For createElement("a") they want HTMLAnchorElement; for createElement("canvas"), HTMLCanvasElement; for any unknown string, HTMLElement. A single signature returning HTMLElement loses the per-tag precision, and a union return forces every caller to re-narrow. Overloads are the tool that makes one function expose several precise contracts at once — but only if you understand exactly how the compiler chooses which contract a given call gets.
The shape: many signatures, one body
A function overload is several call signatures stacked directly above one implementation signature. The call signatures are what callers see. The implementation signature is what the body type-checks against — and it is invisible from the outside.
// Overload signatures (the public contract)
function len(x: string): number;
function len(x: unknown[]): number;
// Implementation signature (internal — NOT a callable overload)
function len(x: string | unknown[]): number {
return x.length;
}
len("hello");
// ^? number
len([1, 2, 3]);
// ^? number
len(123);
// Error: No overload matches this call.
// Argument of type 'number' is not assignable to parameter of type 'string'.The third call fails even though the implementation parameter is string | unknown[] and 123 is neither. The key rule: callers can only call the overload signatures. The implementation signature widens the body’s view but is not part of the public type.
Resolution order: top-down, first match wins
Why does order matter at all? Because the compiler does not search for the best-fitting overload — it stops at the first that fits, so placing a general signature above a specific one silently shadows the specific one forever.
When you write a call, the compiler walks the overload list from the top and binds to the first signature whose parameters accept the arguments. It does not search for the “best” match — it takes the first that fits. Order is therefore load-bearing: put more specific signatures before more general ones.
function pick(x: "a"): 1;
function pick(x: string): 2;
function pick(x: string): 1 | 2 {
return x === "a" ? 1 : 2;
}
pick("a");
// ^? 1 — first signature matches, search stops
pick("b");
// ^? 2 — first didn't match the literal, second doesFlip the two signatures and pick("a") resolves to 2, because string (now first) already accepts "a" and the search never reaches the literal overload.
The implementation signature must cover every overload
The body type-checks against the implementation signature, so that signature must be compatible with — a supertype of — every overload. Each overload’s parameters must be assignable to the impl parameters, and each overload’s return must be assignable to the impl return. If the impl signature is too narrow, the overloads that don’t fit it are rejected.
function f(x: string): string;
function f(x: number): number;
function f(x: string): string {
// ^^^^^^^^^ Error: This overload signature is not compatible
// with its implementation signature.
return x;
}Here the x: number overload promises to accept a number, but the implementation only accepts string. The fix is to widen the impl parameter to string | number (and usually the return to string | number). The implementation signature is not type-checked against callers — it can be looser — but it is checked against the overloads it backs.
▸Why this works
Why isn’t the impl signature callable? Because it exists to type the body, not the interface. If it were callable, an overloaded len(x: string | unknown[]) would let len(maybeStringOrArray) through even when you wanted to forbid the union at the call site. Keeping the impl private lets the public signatures be strictly narrower than what the body technically tolerates — that’s the entire point of overloading.
When overloads beat a union return — and when they lose
Overloads earn their keep when the return type depends on the specific argument type in a way a plain union can’t express. createElement("a") → HTMLAnchorElement vs createElement("canvas") → HTMLCanvasElement is the canonical case: a single signature returning HTMLElement would force every caller to cast.
But overloads have real costs: they don’t relate the parameter and return types generically, so a call with a union argument matches no overload, and they’re verbose and easy to get unsound. Often a single generic or a conditional return type is cleaner:
// Overloads: three signatures, no relationship the compiler can reuse
function wrapO(x: string): string[];
function wrapO(x: number): number[];
function wrapO(x: string | number): (string | number)[] {
return [x];
}
// One generic: relates input and output once, scales to any T
function wrap<T>(x: T): T[] {
return [x];
}
wrap("hi");
// ^? string[]
wrap(42);
// ^? number[]
const u: string | number = Math.random() > 0.5 ? "x" : 1;
wrapO(u);
// Error: No overload matches this call — a union argument fits no single overload.
wrap(u);
// ^? (string | number)[] — the generic handles the union for freeReach for overloads only when the input/output mapping is discrete and irregular (different literal inputs → unrelated output types). When the mapping is uniform, prefer a generic; when it’s rule-based on the type, prefer a conditional return type. Both compose better and stay sound.
You write overload signatures `(x: string): 'wide'` then `(x: 'id'): 'narrow'` (in that order), backed by a compatible impl. What does `f('id')` resolve to, and why?
Order the steps the compiler takes to type a call against an overloaded function.
- 1 Collect the overload (call) signatures in source order, ignoring the implementation signature
- 2 Start at the first overload signature
- 3 Check whether the call's arguments are assignable to that signature's parameters
- 4 On the first signature that matches, bind the call and stop searching
- 5 If no overload matches, report 'No overload matches this call'
- 01Walk through exactly how the compiler resolves a call against an overloaded function, and why overload order matters.
- 02When should you reach for overloads, and when is a generic or conditional return type the better tool?
Function overloads expose several public call signatures over a single private implementation signature. The compiler resolves a call by walking the overload signatures top-to-bottom and binding the first that accepts the arguments — first-match, not best-match — which is why specific signatures must precede general ones. The implementation signature must be a supertype of every overload and is never callable from outside the function. Use overloads only for discrete, irregular input→output mappings; reach for a generic or conditional return type otherwise. Next we leave the parameters and look at the hidden first parameter every method secretly has: this, how TypeScript types it with the fake this: T parameter, and how it gets lost when a method is passed as a callback. Now when you see an overloaded function that returns an unexpected type for a literal argument, check the signature order first — the specific overload may be sitting below a general one that swallows it.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.