skip to content

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%

answer

  1. one signature chosen, unions never split
  2. the error is telling the caller the truth
  3. narrow before calling
  4. does the return type track the form?
  5. overload count multiplies, one signature does not

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.

solid answer

~50 s

The compiler selects exactly one overload per call; it never combines two headers to cover a union argument. Since neither `(x: string)` nor `(x: number)` accepts `string | number`, the call fails with `No overload matches this call.` Three ways out. Narrow at the call site — inside a `typeof` branch the argument is a single type and resolution succeeds; this is usually right, because the caller genuinely does not know which result type it is getting. Add a trailing header taking `string | number` and returning the union: legal, but callers lose the correlation and must re-narrow the result. Or abandon overloads for one union-parameter signature. The deciding question is whether the return type actually depends on which form was used. If it does, overloads pay for themselves; if it does not, they add order-dependence and an unchecked promise for nothing.

code

typescript · 14 lines
typescript
function format(value: string): string;
function format(value: number): number;
function format(value: string | number): string | number {
  return typeof value === "string" ? value.trim() : Math.round(value);
}

declare const raw: string | number;
// format(raw); // error: No overload matches this call.

if (typeof raw === "string") {
  console.log(format(raw)); // string
} else {
  console.log(format(raw)); // number
}

go deeper

for a junior

Know that passing a string | number value to a function overloaded for string and for number is a compile error, and that checking the type with typeof before the call makes it work.

for a middle

Explain that exactly one signature is selected and unions are never split across headers, then walk the three fixes — narrow at the call site, append a union header, or collapse to one signature — and what each costs.

for a senior

Own the choosing rule in real designs: overloads only when the return type tracks the argument form, one signature when it does not, a generic when the result follows the caller's own type. Cite the combinatorics and the unverified-promise cost.

for a principal

Judge the public surface: an overload list is order-dependent, multiplicative and unproven, so decide deliberately when its call-site ergonomics justify that in a library others depend on, and set the team's default the other way.

## Why the union argument fails Overload resolution picks **one** signature. The compiler tries each header in order and asks whether the arguments are assignable to it; it never merges two headers into a combined signature that would accept a union. A value typed `string | number` is assignable to neither `(x: string)` nor `(x: number)`, so nothing matches and you get `No overload matches this call.` ```ts function format(value: string): string; function format(value: number): number; function format(value: string | number): string | number { return typeof value === "string" ? value.trim() : Math.round(value); } declare const raw: string | number; // format(raw); // No overload matches this call. ``` This is not an oversight. If the compiler did split the union across headers, it would have to produce `string | number` as the result — which is exactly the correlation the overloads existed to preserve. Refusing the call keeps the promise honest and pushes the decision back to the caller. ## Option one: narrow at the call site ```ts if (typeof raw === "string") { const s = format(raw); // string } else { const n = format(raw); // number } ``` Inside each branch the argument has a single type, so resolution succeeds and the caller gets the precise result type. This is usually the right answer, because the caller in the union case genuinely does not know which result type it will receive — the compile error is telling the truth about the caller's own uncertainty, not obstructing it. ## Option two: add a union header Append a header covering the union, below the precise ones: ```ts function format(value: string): string; function format(value: number): number; function format(value: string | number): string | number; ``` Now the union call compiles, but the result is `string | number` and the caller must re-narrow it. That is honest, and it keeps precise calls precise because ordering puts the narrow headers first. The cost is a third unverified promise about the same body and a wider public surface. ## Option three: drop overloads for one signature If you find yourself adding the union header for every combination, that is a signal the overload list is not earning its place: ```ts function format(value: string | number): string | number { /* ... */ } ``` One signature, no ordering hazard, nothing unchecked. Callers narrow the result, which they were going to do anyway. ## The choosing rule Ask one question: **does the return type depend on which argument form was used?** - **It does, and the forms are a small fixed set.** Overloads are the right tool. Different arities that mean different things belong here too. - **It does not — every form returns the same type.** Use a single signature with a union or optional parameters. Overloads buy nothing and cost order-dependence plus an unproven per-form promise. - **The result type is a function of the argument's own type, not of a fixed list of forms.** That is a generic's job — a type parameter carries the caller's type through to the result, and unlike an overload list it scales to types you have never enumerated. Overloads cannot express "whatever came in comes back" for open-ended input, and a growing overload list that keeps repeating the same shape is the symptom. ## Practical differences that decide close calls **Call sites.** Overloads give the cleanest error messages and the cleanest editor hints for a small set of forms: the tooltip lists the legal shapes. A generic with an involved constraint often shows the caller a signature that is harder to read. **Combinatorics.** Overloads multiply. Two independent optional dimensions become four headers, and each header is a separate promise about the same body. A generic or a single union signature stays flat. **Union arguments.** As above, overloads reject them by design. If your callers routinely hold union-typed values — data coming out of a parser, a config lookup, a message queue — an overload list will fight them at every call, and a single signature that returns a union they narrow is friendlier. **Verification.** A union-parameter signature is checked normally against the body. An overload header is not: nothing proves the branch serving it returns what it promised. That asymmetry alone should make overloads the exception rather than the default. ## What to say in an interview Lead with the mechanism — one signature is chosen, unions are never split across headers — then give the three options and the choosing rule in the same breath. The mark of experience is not knowing that overloads exist but saying out loud when you would *not* use them: same return type across forms, open-ended input types, or callers who mostly hold unions.

  • If you add a trailing header accepting `string | number`, what do callers of the precise forms lose?
    Nothing, provided the narrow headers stay above it — first-match resolution still sends a plain string to the string header. The union caller gains a legal call but receives `string | number` and must narrow the result. You have also added a third unverified promise about the same body.
  • When is a generic clearly the better tool than an overload list here?
    When the result type is a function of the caller's own type rather than of a fixed menu of forms — a lookup returning the value type for a given key, or a helper that returns whatever it was handed. Overloads must enumerate every case, so an open-ended set of input types becomes an ever-growing list you cannot complete.
  • Your overload list has grown to nine headers covering combinations of optional parameters. What does that tell you?
    That the forms are independent dimensions, not distinct operations. Overloads scale multiplicatively, and each header is another unchecked promise about one body. Collapse them into a single signature taking an options object, or split the genuinely different behaviours into separately named functions.

saying these in an interview costs you the question

  • Says a union argument works because it matches both overloads
  • Reaches for an assertion to force the union call through
  • Adds a broad union header at the top of the list
  • Uses overloads when every form returns the same type
  • Thinks overloads and generics are interchangeable for open-ended input

context