skip to content

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%

answer

  1. body sees implementation types only
  2. two separate checks, loosely related
  3. `any` return makes every header compatible
  4. the promise per form is never proven
  5. tests are the only real guarantee

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.

solid answer

~50 s

The compiler does two separate things. It checks the body against the implementation signature alone — inside the body, parameters have the implementation's types, not any overload's. Separately it checks each overload header for compatibility with the implementation, reporting `This overload signature is not compatible with its implementation signature.` when a header advertises something the implementation could not possibly accept. What it never does is tie a specific overload to the branch that serves it: nothing verifies that the code path taken for the `string` form actually returns what the `string` overload promised. If the implementation's return type is `any`, or merely related to the overload's, the promise passes unchecked. So an overload list is a soundness hole you own: keep the implementation's return type as tight as the overloads allow, and cover each advertised form with a test.

code

typescript · 8 lines
typescript
function parse(input: string): string[];
function parse(input: number): number[];
function parse(input: string | number): any {
  return typeof input === "string" ? input.split(",") : [String(input)];
}

const nums = parse(7); // declared number[], actually ["7"]
console.log(nums.length);

go deeper

for a junior

Know that the body is written against the implementation signature, so inside it a parameter has the implementation's type — not the type from whichever form the caller used.

for a middle

Explain both checks and their limits: headers must be compatible with the implementation, the body is checked only against the implementation, and no check ties a branch to the header it serves. Name what an any return type erases.

for a senior

Show how you contain the hole in real code: a tight implementation return type, a test per advertised form, and scepticism toward overloads on a body that is hard to reason about. Be able to describe a production bug this class of gap causes.

for a principal

Frame overloads as unverified claims embedded in a published API. Decide when the ergonomic win justifies an unchecked promise every consumer inherits, and set a team rule for how such promises are reviewed and tested.

## Two checks, not one An overloaded function involves two distinct pieces of type-checking, and confusing them is the source of most surprises. **Check one: the body against the implementation signature.** Inside the body, each parameter has the type written on the implementation signature — never the type from whichever overload the caller happened to match. There is no per-overload type-checking pass over the body; the body is compiled once, as one function. **Check two: each overload header against the implementation signature.** The compiler requires that every advertised form is something the implementation could actually serve. When it is not, you get error 2394: `This overload signature is not compatible with its implementation signature.` ```ts function handle(input: string): void; function handle(input: boolean): void; // error 2394 function handle(input: string | number): void { console.log(input); } ``` The second header promises callers may pass a boolean, but the implementation only accepts `string | number`, so the compiler rejects the header rather than the call site. ## The compatibility check is deliberately loose That second check is far weaker than people expect, especially about return types. It is satisfied when the implementation's return type and the overload's return type are related in *either* direction — the implementation may return something wider than the overload promises, and the check still passes. The most extreme case is an implementation typed to return `any`, which is compatible with every overload return type by construction and therefore checks nothing at all. ## The gap: nobody proves the promise Put the two facts together and the hole appears. The overload signature makes a promise to callers about the return type for a given argument form. The body is checked only against the implementation signature. **Nothing connects a branch of the body to the overload it is meant to serve.** ```ts function parse(input: string): string[]; function parse(input: number): number[]; function parse(input: string | number): any { return typeof input === "string" ? input.split(",") : [String(input)]; } const nums = parse(7); // typed number[] console.log(nums[0].toFixed(2)); // compiles; throws at run time ``` `parse(7)` is typed `number[]` because that is what the second overload promised, but the body returns an array of strings for the numeric branch. The `any` return type on the implementation made both overloads trivially compatible, so nothing complained. Downstream code then calls a number method on a string and fails at run time. This is the same family of unsoundness as a type assertion: the compiler trusts a claim it cannot verify. ## How to close it - **Do not type the implementation's return as `any`.** Give it the union of the overload return types — `string[] | number[]` above. That will not verify per-branch correctness either, but it stops arbitrarily wrong return values from slipping through, and it is enough to catch the mistake in the example. - **Keep the implementation signature as narrow as the overloads allow.** A wide implementation signature buys nothing publicly and weakens every check. - **Test each advertised form.** Because the promise is unproven at compile time, a test per overload is the only thing that actually holds it. A type-level assertion at the call site helps too: assign the result to a variable of the declared type and let the checker confirm the declaration you meant. - **Prefer fewer forms.** Each overload is one more unverified claim about the same body. ## Related pitfalls The implementation signature is also not callable, so widening it to make a header compatible does not add a public form — the only way to advertise a form is another header. And a header that is compatible but *unreachable* — shadowed by an earlier, broader header — is not reported at all; the compatibility check says nothing about ordering. ## What to say in an interview The crisp framing is: overload headers describe the public type; the implementation signature type-checks the body; the compiler relates the two only loosely and never proves that the branch serving one header returns what that header promised. Treat an overload list as an assertion you must back with tests, not as a checked guarantee.

  • What concretely changes if you type the implementation's return as the union of the overload return types instead of `any`?
    The body must return something assignable to that union, so wholesale wrong values are caught. It still does not prove that the branch serving the numeric form returns the numeric variant — correlation between argument form and result stays unverified — but it removes the blanket escape hatch `any` provides.
  • How does this unsoundness compare with an `as` assertion?
    They are the same kind of hole with different ergonomics: both are claims the compiler accepts without proof and neither costs anything at runtime. An overload list is arguably worse, because the claim lives on the declaration and every caller inherits it, whereas an assertion is visible at the one site that made it.
  • Can you get the compiler to check the body per overload by writing separate branches with their own signatures?
    Not with an overload list — there is one body and one signature governing it. If per-form checking matters, publish separate named functions, each with its own signature and body; the compiler then checks each one properly. You trade the single call name for real verification.

saying these in an interview costs you the question

  • Believes the body is re-checked against each overload signature
  • Types the implementation's return as `any` and calls the overloads safe
  • Thinks a compatible implementation proves the declared return types
  • Expects error 2394 to catch a wrong value returned by a branch
  • Widens the implementation signature to silence 2394 without adding a header

context