Function Overloads and Call Signatures
Describing a function with several legitimate shapes, ordering overloads so the right one wins, and when a union or generic beats overloading entirely.
Describing a function with several legitimate shapes, ordering overloads so the right one wins, and when a union or generic beats overloading entirely.
A function returns a Photo for an id and a Photo[] for an
array of ids. Typed with a union return, every caller narrows a
result they already knew the shape of. Typed with overloads, the
signature grows a case per variant and the implementation
underneath is checked against none of them properly.
Both are real tools with real failure modes. By the end of this lesson you will know when a function genuinely has several shapes, how overloads are resolved, and why a generic or a discriminated argument is usually the better answer.
function load(id: string): Photo;
function load(ids: string[]): Photo[];
function load(input: string | string[]): Photo | Photo[] {
return Array.isArray(input) ? input.map(loadOne) : loadOne(input);
}const one = load("a"); // Photo
const many = load(["a", "b"]); // Photo[]The first two lines are overload signatures — the shapes callers see. The third is the implementation signature, which is not callable and must be compatible with all of them.
Three rules govern resolution.
First match wins. Signatures are tried in order, so more specific ones must come first:
function get(key: "count"): number;
function get(key: string): unknown; // must be secondReversed, the general signature matches everything and the specific one is unreachable.
The implementation signature is invisible. Callers cannot use it, so a union in it does not make a union call legal:
declare const maybe: string | string[];
load(maybe);
// No overload matches this call.That is the first real cost. A caller with a value of the union type has to narrow it before calling — even though the implementation handles both.
The implementation is barely checked against its overloads, which is the second cost:
function load(id: string): Photo;
function load(ids: string[]): Photo[];
function load(input: string | string[]): Photo | Photo[] {
return loadOne(input as string); // wrong for arrays
}That compiles. The overloads promise one thing, the body does another, and nothing connects them. Every overload set is a small unverified claim.
Bad — overloads for what is one computation.
function first(items: string[]): string | undefined;
function first(items: number[]): number | undefined;
function first(items: Photo[]): Photo | undefined;
function first(items: unknown[]): unknown {
return items[0];
}Good — one generic signature.
function first<T>(items: readonly T[]): T | undefined {
return items[0];
}The overloaded version needs a new line per type — so a caller
with Album[] gets "no overload matches" for a function that
would work perfectly. And the implementation returns unknown,
so its correctness against any of the three is unchecked.
The generic covers every element type, checks the body once, and is shorter. If the overloads differ only in a type that could be a parameter, use a generic.
The other frequent case is options that change the return:
function read(path: string, opts: { parse: true }): Photo;
function read(path: string, opts?: { parse?: false }): string;
function read(path: string, opts?: { parse?: boolean }): Photo | string {
...
}A conditional type does it with one signature:
function read<P extends boolean = false>(
path: string,
opts?: { parse?: P },
): P extends true ? Photo : string { ... }That is the conditional-types lesson applied. It scales to more options where overloads multiply.
Three cases where nothing else fits.
Genuinely unrelated signatures
Different parameter counts with different meanings, where no single signature describes both.
A callback shaped by an earlier argument
Where a conditional type would be harder to read than three lines of overload.
Describing something you did not write
In a declaration file, where the runtime behaviour is what it is and your job is to describe it accurately.
The first case, concretely:
Two guidelines when you do use them. Order from most to least
specific. And make the implementation signature as narrow as the
union of the overloads — unknown there gives up the little
checking available.
A function type can be written as an object with a call signature, which allows properties alongside:
Multiple call signatures in one type are overloads:
And a construct signature describes something used with
new:
That is how you type a function that receives a class rather than an instance — a factory, a dependency container, a plugin registry.
A function's this can be declared as a first pseudo-parameter:
It is erased and does not affect the arguments. With
strictBindCallApply and noImplicitThis on, this is what
makes a detached method a compile error rather than a runtime
one:
Which is the classic class bug from the foundations course,
caught by the checker rather than at runtime — but only when the
this type is declared.
You can now describe a function with several legitimate shapes, and — more often useful — recognise when a generic or a conditional type expresses the same thing with one signature and real checking. The unverified implementation is the reason to prefer them: an overload set is a promise nobody checks.
Next is Type-Level Programming and Its Limits, which takes the machinery from these lessons to its edge and asks the question this catalog keeps returning to: at what point does a clever type cost more than the bugs it prevents.
Before you move on, find an overload set in your code and try rewriting it as a generic or a conditional type. If it collapses to one signature, it was never really several functions — and you have just removed a claim nothing was checking.
OVERLOADS
function f(a: string): X; the shapes callers see
function f(a: string[]): Y;
function f(a: string | string[]): X | Y { ... } not callable
FIRST MATCH WINS - most specific first, or the general one
makes the rest unreachable
the implementation signature is INVISIBLE to callers, so a
caller holding the union must narrow before calling
the implementation is barely checked against the overloads -
each set is a small unverified claim
PREFER INSTEAD
a generic when overloads differ only in a type
a conditional when an option decides the return type
a union parameter when one signature genuinely describes it
overloads that differ only by element type should be <T>
WHEN OVERLOADS ARE RIGHT
genuinely unrelated shapes (different arities and meanings)
a callback whose shape depends on an earlier argument
describing code you did not write, in a .d.ts
order specific -> general
keep the implementation signature narrow, not `unknown`
CALL SIGNATURES
{ (req: Request): Response; name: string } callable + properties
several call signatures = overloads
{ new (id: string): Photo } a CLASS, not an instance
- for factories and DI
this
function f(this: Store): void a pseudo-parameter, erased
with noImplicitThis, a detached method becomes a compile errorfunction createElement(tag: "canvas"): HTMLCanvasElement;
function createElement(tag: "video"): HTMLVideoElement;
function createElement(tag: string): HTMLElement;type Middleware = {
(request: Request): Response;
name: string;
priority: number;
};
const auth: Middleware = Object.assign(
(request: Request) => handle(request),
{ name: "auth", priority: 1 },
);type Loader = {
(id: string): Photo;
(ids: string[]): Photo[];
};type PhotoClass = {
new (id: string, size: number): Photo;
readonly MAX_SIZE: number;
};
function build(Cls: PhotoClass): Photo {
return new Cls("a", 1);
}function reset(this: PhotoStore): void {
this.items.clear();
}const fn = store.reset;
fn();
// The 'this' context of type 'void' is not assignable