skip to content

Overload Signatures

Listing several call signatures gives one function multiple public shapes, resolved top-down against the first match, with the implementation signature deliberately invisible to callers. Interviewers ask when overloads beat a union parameter or a generic — usually when the return type depends on which argument form was used.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

4

In TypeScript, in what order does the compiler try the signatures in an overload list, and what goes wrong when a broader signature is written above a narrower one?

level: middleimportance: must knowfreq 55%

answer

  1. top-down, first match wins
  2. no specificity ranking
  3. literal above primitive
  4. a shadowed header is silent dead code
  5. catch-all belongs last

basics

~20 s

TypeScript reads an overload list top-down and commits to the first signature the arguments satisfy — not the most specific one. A broader signature listed above a narrower one therefore shadows it, and the narrower form's return type is never seen.

solid answer

~50 s

Overload resolution walks the list in declaration order and stops at the first signature the call is assignable to. It does not rank candidates by specificity, so ordering is semantics, not style. The classic bug is a header taking `string` written above one taking a literal like `"retries"`: every call, including `getSetting("retries")`, satisfies the `string` header first, so you get the broad return type and the narrow overload becomes dead code the compiler never warns about. The fix is to order signatures from most specific to most general — literal types before their primitive, fixed arities before rest or optional forms, concrete object shapes before wide ones. When a call matches nothing, the compiler reports `No overload matches this call.` and, for diagnostics, elaborates against one candidate — usually the last — which is why the error often names a signature you did not intend.

code

typescript · 8 lines
typescript
function getSetting(key: string): unknown;
function getSetting(key: "retries"): number;
function getSetting(key: string): unknown {
  return key === "retries" ? 3 : undefined;
}

const retries = getSetting("retries");
console.log(retries); // typed unknown: the broad header matched first

go deeper

for a junior

Remember that overloads are tried from top to bottom and the first one that fits wins, so the order you write them in matters and is not just formatting.

for a middle

Explain first-match resolution, show a literal-versus-primitive pair where the broad header shadows the narrow one, and give the ordering heuristics — literals first, catch-alls last — plus the fix.

for a senior

Demonstrate that adding a header to an existing list can silently retype call sites with no error at the declaration, and describe the type-level assertions you use to keep an overload list honest as it grows.

for a principal

Treat order-dependent resolution as an API-stability risk: an overload list is a contract whose meaning changes with edits nobody flags. Decide when a simpler signature or separate named functions is the safer public surface.

## The rule When a function has an overload list, the compiler tries the signatures **in the order they are written** and commits to the **first** one the call's arguments are assignable to. It does not gather all viable candidates and pick the most specific; there is no specificity ranking and no ambiguity error. First match wins. That single rule turns the order of an overload list into part of the function's meaning. Reordering two headers can silently change the type of every call site. ## The ordering bug ```ts function getSetting(key: string): unknown; function getSetting(key: "retries"): number; function getSetting(key: string): unknown { return key === "retries" ? 3 : undefined; } const retries = getSetting("retries"); // unknown, not number ``` The literal `"retries"` is assignable to `string`, so the first header matches and resolution stops there. The result is `unknown`, and any arithmetic on it fails to compile with an error that looks like it is about the wrong thing. The second header is unreachable — dead code the compiler does not flag. Swap the two headers and `getSetting("retries")` is `number` while other keys still fall through to the general form. The same shadowing appears with arity and optionality: ```ts function on(event: string, handler: (...args: unknown[]) => void): void; function on(event: "click", handler: (x: number, y: number) => void): void; ``` Every call matches the first header, so the precise `"click"` handler typing never applies. ## Ordering heuristics that work - **Literal types before the primitive they belong to** — `"retries"` above `string`, `0` above `number`. - **Fixed arities before variadic or optional forms** — a two-parameter header above one ending in a rest parameter. - **Concrete object shapes before wide ones** — a header taking a specific interface above one taking `Record<string, unknown>`. - **Anything taking `any` or `unknown` goes last**, because such a header matches almost every call and will swallow the list. A useful mental check: read the list top to bottom and ask, for each header, "is there a call this header accepts that a header above it also accepts, but which should have gone to mine?" If yes, move it up. ## When nothing matches If no signature accepts the call, the compiler reports `No overload matches this call.` It then elaborates by explaining why one particular candidate failed — usually the last one — which is why the message often complains about a signature you never intended to use. Read the whole diagnostic, not the last line: the elaboration is a report about one candidate, not a claim that the others were closer. Ordering also interacts with the fact that only the headers participate in resolution. The implementation signature is never a candidate, however permissive it looks, so a call that "obviously should work" because the implementation accepts it will still be rejected. ## Why the compiler works this way First-match resolution is simple, predictable and cheap: the checker never has to define what "most specific" means across unions, optionality, generics and structural subtyping — a relation that is not even a total order in a structural type system. The cost is pushed onto the author, who must express intent through ordering. That is a deliberate trade, and it is why every review of an overload list should look at the order first. ## Consequences for API design Because order is meaning, an overload list is fragile in ways a single signature is not. Adding a convenience header at the top of an existing list can silently retype every call site in a codebase and produce no error at the definition. Prefer appending new, narrower headers **above** the general catch-all and below any narrower ones, and treat the list's order as covered by tests: a type-level check per advertised form — assign each call's result to a variable of the expected type — catches a shadowed header immediately, which is the only thing that will. ## What to say in an interview State the rule, then demonstrate the shadowing bug with a literal-versus-`string` pair and show the fix by reordering. Adding that the compiler performs no specificity ranking, and that a shadowed overload is never reported as dead, is what separates a memorised answer from an understood one.

  • Does the compiler warn you that a shadowed overload can never be selected?
    No. There is no unreachable-overload diagnostic, so a header hidden by a broader one above it simply never applies and nothing is reported. You notice only through the surprising type at a call site, or through a type-level test that asserts the expected result type for each advertised form.
  • When a call matches nothing, why does the error often quote a signature you never meant to use?
    `No overload matches this call.` is followed by an elaboration explaining why a single candidate failed — commonly the last one. That elaboration is a diagnostic aid, not a statement that the quoted candidate was the closest match, so read the whole message before assuming which form you missed.
  • Is adding a new overload at the top of an existing list a safe change?
    No. Because order decides resolution, a broad header inserted at the top can capture calls that previously matched a lower, narrower header and silently retype them. Nothing fails at the declaration. Add narrow headers above the catch-all but below anything narrower still, and re-run type-level assertions on the existing forms.

saying these in an interview costs you the question

  • Says the compiler picks the most specific matching overload
  • Treats overload order as a style choice, not semantics
  • Puts a `string` or `any` header above literal-typed ones
  • Expects a warning when an overload becomes unreachable
  • Assumes the signature named in the error was the closest match

context

open as a page

In TypeScript, a function is written as two overload signatures followed by one implementation signature. How many functions does the emitted JavaScript contain, and which of those signatures can a caller use?

level: juniorimportance: should knowfreq 50%

basics

~20 s

The emitted JavaScript contains exactly one function — overload signatures are types and are erased. Callers may use only the overload signatures; the implementation signature is invisible to them and cannot be called on its own terms.

open as a page

In TypeScript, the body of an overloaded function is type-checked against its implementation signature only. Given that, what does the compiler verify between the overload signatures and the implementation, and what does it not verify?

level: middleimportance: should knowfreq 45%

basics

~20 s

TypeScript checks only that each overload signature is compatible with the implementation signature, and that check is deliberately loose. It never verifies that the body returns the type a particular overload promised, so overloads are an unchecked promise.

open as a page

In TypeScript a function is overloaded as `(x: string): string` and `(x: number): number`. A caller holds a value typed `string | number` and passes it — why is that a compile error, and what are the options?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Overload resolution commits to one signature, and neither accepts a union, so a string | number argument matches nothing and the compiler reports no matching overload. Fix it by narrowing at the call site, or drop overloads for a single union-parameter signature.

open as a page