TypeScript Overloads Describe Calls, Not Implementations
How overload sets are selected, what callers can see, and why utility types inspect the final signature.
Function overloads let one JavaScript implementation expose several call relationships. They are valuable when an argument shape determines a return shape more precisely than a broad union signature can express.
function parse(value: string): object;
function parse(value: Uint8Array): object;
function parse(value: string | Uint8Array): object {
const text = typeof value === 'string'
? value
: new TextDecoder().decode(value);
return JSON.parse(text);
}
The declarations without bodies are overload signatures. The signature with the body is the implementation signature.
Callers see all three signatures
The implementation signature must be compatible with the overloads because it has to handle every advertised call. It is also available to callers as a final fallback.
declare const input: string | Uint8Array;
parse(input); // accepted by the implementation signature
This is useful when a caller already has the same union used internally. Adding a broad implementation signature therefore widens the public API even when the preceding overloads are narrow.
Resolution selects one compatible overload
At a call site, TypeScript considers overload signatures in source order and chooses a compatible candidate according to its resolution rules. More specific signatures should generally appear before broad ones so they can preserve useful return types.
function create(tag: 'button'): HTMLButtonElement;
function create(tag: string): HTMLElement;
function create(tag: string): HTMLElement {
return document.createElement(tag);
}
create('button') receives HTMLButtonElement, while a general string receives HTMLElement. The runtime does not know which overload was selected; the implementation still performs one ordinary JavaScript dispatch.
The body is checked against its own parameters
Inside the implementation, overloads do not narrow the parameters automatically. The body sees the implementation signature and must use normal runtime guards.
Returning unknown from the implementation can be a useful technique when overloads promise different types, because it forces the body to establish each result deliberately. Returning any removes that internal check and can conceal mismatches.
Arrow functions do not have overload declaration syntax, but an overloaded callable interface can describe an arrow value:
interface Convert {
(value: string): number;
(value: number): string;
}
const convert: Convert = (value: string | number) =>
typeof value === 'string' ? value.length : String(value);
Type-level inspection uses the first overload
Utilities such as ReturnType<T> infer from the first overload signature because it is the most specific advertised call.
declare function lookup(id: number): { id: number };
declare function lookup(name: string): { name: string };
declare function lookup(value: string | number): object;
type LookupResult = ReturnType<typeof lookup>;
// { id: number }
This means overload ordering controls both call resolution and conditional-type inference. A final catch-all signature affects runtime-shaped utilities only when no earlier signature applies.
Use overloads when they preserve a meaningful relationship between inputs and outputs. For a parameter that is simply one of several accepted types with the same return, a union is easier for both callers and implementations. For repeated structural patterns, a generic or conditional type may scale better than a long overload list.