Overloading
Overloading gives one function several type signatures, so a call is checked against the shape it actually has rather than against the union of every shape the function accepts. Without it, a function that takes either one argument or two has a type that permits calls it cannot serve.
TypeScript has four ways to write this, only one of which is called overloading. And the first question is always whether two functions with different names would be better, because usually they would. The function types page is the entry for a function's type when it has only one signature.
Create programs/overloading.test.ts for this reference and keep it open. Start it with the assertion helpers used on this page:
import { assertStrictEquals, assertThrows } from "@std/assert";
Below the import, add the fixtures every step shares: a Customer, one customer, and a map of them.
type Customer = { fullName: string };
const ada: Customer = { fullName: "Ada Lovelace" };
const customers = new Map<string, Customer>([
["1234", ada],
["5678", { fullName: "Lars Croft" }],
]);
Follow the page as you add and revise the runnable examples below the fixtures.
the problem overloading solves
One function that takes a customer, or a map and an id. Its single signature is honest about what it accepts and useless about what it requires, so predict the call that mixes the two shapes:
Deno.test("the problem overloading solves", () => {
function looseFullName(
customerOrMap: Customer | Map<string, Customer>,
id?: string,
): string {
if (customerOrMap instanceof Map) {
if (id === undefined) throw new TypeError("an id is required with a map");
const found = customerOrMap.get(id);
if (found === undefined) throw new TypeError(`unknown id: ${id}`);
return found.fullName;
}
if (id !== undefined) throw new TypeError("an id makes no sense here");
return customerOrMap.fullName;
}
assertStrictEquals(looseFullName(ada), "Ada Lovelace");
assertStrictEquals(looseFullName(customers, "1234"), "Ada Lovelace");
assertStrictEquals(looseFullName(ada, "5678"), "Lars Croft");
});
Check programs/overloading.test.ts
running 1 test from ./programs/overloading.test.ts
the problem overloading solves ... FAILED (694µs)
ERRORS
the problem overloading solves => ./programs/overloading.test.ts:27:6
error: TypeError: an id makes no sense here
if (id !== undefined) throw new TypeError("an id makes no sense here");
^
FAILURES
the problem overloading solves => ./programs/overloading.test.ts:27:6
FAILED | 0 passed | 1 failed (1ms)
error: Test failed
It compiled, and it cannot work. A union parameter plus an optional parameter describes four combinations, of which two are nonsense, and the body is left to throw at run time for the mistakes the type should have caught: a customer with an id, and a map without one. Correct the step by pinning both bad calls as the runtime refusals they are:
Deno.test("the problem overloading solves", () => {
function looseFullName(
customerOrMap: Customer | Map<string, Customer>,
id?: string,
): string {
if (customerOrMap instanceof Map) {
if (id === undefined) throw new TypeError("an id is required with a map");
const found = customerOrMap.get(id);
if (found === undefined) throw new TypeError(`unknown id: ${id}`);
return found.fullName;
}
if (id !== undefined) throw new TypeError("an id makes no sense here");
return customerOrMap.fullName;
}
assertStrictEquals(looseFullName(ada), "Ada Lovelace");
assertStrictEquals(looseFullName(customers, "1234"), "Ada Lovelace");
assertThrows(
() => looseFullName(customers),
TypeError,
"an id is required with a map",
);
assertThrows(
() => looseFullName(ada, "5678"),
TypeError,
"an id makes no sense here",
);
});
the problem overloading solves ... ok (432µs)
two signatures and one implementation
Add the overloaded version at module scope, below the fixtures, since later steps call it too:
function fullName(customer: Customer): string;
function fullName(map: Map<string, Customer>, id: string): string;
function fullName(
customerOrMap: Customer | Map<string, Customer>,
id?: string,
): string {
if (customerOrMap instanceof Map) {
if (id === undefined) throw new TypeError("an id is required with a map");
const found = customerOrMap.get(id);
if (found === undefined) throw new TypeError(`unknown id: ${id}`);
return found.fullName;
}
if (id !== undefined) throw new TypeError("an id makes no sense here");
return customerOrMap.fullName;
}
Two overload signatures, which have no bodies, followed by one implementation signature, which has the body and must be compatible with both. The two bad calls are now compile errors, and the shields prove the errors were worth having, because each one still throws:
Deno.test("two signatures and one implementation", () => {
assertStrictEquals(fullName(ada), "Ada Lovelace");
assertStrictEquals(fullName(customers, "5678"), "Lars Croft");
assertThrows(
() => {
// @ts-expect-error: Argument of type 'Map<string, Customer>' is not assignable to parameter of type 'Customer'.
fullName(customers);
},
TypeError,
"an id is required with a map",
);
assertThrows(
() => {
// @ts-expect-error: Argument of type 'Customer' is not assignable to parameter of type 'Map<string, Customer>'.
fullName(ada, "5678");
},
TypeError,
"an id makes no sense here",
);
});
two signatures and one implementation ... ok (63µs)
That is the whole feature. Everything below is the details and the alternatives.
the implementation signature is not one of the overloads
Hand fullName a value that is one shape or the other, without knowing which, in a scratch file programs/overloads.ts:
type Customer = { fullName: string };
export function fullName(customer: Customer): string;
export function fullName(map: Map<string, Customer>, id: string): string;
export function fullName(
customerOrMap: Customer | Map<string, Customer>,
id?: string,
): string {
if (customerOrMap instanceof Map) {
if (id === undefined) throw new TypeError("an id is required with a map");
const found = customerOrMap.get(id);
if (found === undefined) throw new TypeError(`unknown id: ${id}`);
return found.fullName;
}
if (id !== undefined) throw new TypeError("an id makes no sense here");
return customerOrMap.fullName;
}
export function refusedCall(either: Customer | Map<string, Customer>): string {
return fullName(either);
}
Check programs/overloads.ts
TS2345 [ERROR]: Argument of type 'Customer | Map<string, Customer>' is not assignable to parameter of type 'Customer'.
Property 'fullName' is missing in type 'Map<string, Customer>' but required in type 'Customer'.
return fullName(either);
~~~~~~
at file:///programs/overloads.ts:20:19
'fullName' is declared here.
type Customer = { fullName: string };
~~~~~~~~
at file:///programs/overloads.ts:1:19TS2793 [ERROR]: The call would have succeeded against this implementation, but implementation signatures of overloads are not externally visible.
export function fullName(
~~~~~~~~
at file:///programs/overloads.ts:5:17
error: Type checking failed.
The TS2793 at the bottom is worth reading twice. The checker knows the call would work against the implementation signature, tells you so, and refuses it anyway. That is the design: the implementation signature is an internal promise about the body, and the overload signatures are the published API, so if you want the loose call, publish a loose signature. Delete the scratch file, and pin both the refusal and the fix:
Deno.test("the implementation signature is not one of the overloads", () => {
function refusedCall(either: Customer | Map<string, Customer>): string {
// @ts-expect-error: Argument of type 'Customer | Map<string, Customer>' is not assignable to parameter of type 'Customer'.
return fullName(either);
}
function narrowedCall(either: Customer | Map<string, Customer>): string {
return either instanceof Map ? fullName(either, "1234") : fullName(either);
}
assertStrictEquals(refusedCall(ada), "Ada Lovelace");
assertThrows(
() => refusedCall(customers),
TypeError,
"an id is required with a map",
);
assertStrictEquals(narrowedCall(ada), "Ada Lovelace");
assertStrictEquals(narrowedCall(customers), "Ada Lovelace");
});
the implementation signature is not one of the overloads ... ok (67µs)
narrowedCall is the fix, and it is ordinary narrowing rather than a workaround, from the unions and narrowing page. It is worth noticing what happened: overloading pushed the work of deciding which shape you have from the function into the caller. Sometimes that is right, and sometimes it is a nuisance.
a union of tuples narrows like ordinary code
The second of the four ways, one signature whose argument list is a union of tuple types. Shield the bad call past the checker and predict what it does at run time:
Deno.test("a union of tuples narrows like ordinary code", () => {
function tupleFullName(
...args: [customer: Customer] | [map: Map<string, Customer>, id: string]
): string {
if (args.length === 2) {
const [map, id] = args;
const found = map.get(id);
if (found === undefined) throw new TypeError(`unknown id: ${id}`);
return found.fullName;
}
const [customer] = args;
return customer.fullName;
}
assertStrictEquals(tupleFullName(ada), "Ada Lovelace");
assertStrictEquals(tupleFullName(customers, "5678"), "Lars Croft");
assertThrows(
() => {
// @ts-expect-error: Argument of type '[Map<string, Customer>]' is not assignable to parameter of type '[customer: Customer] | [map: Map<string, Customer>, id: string]'.
tupleFullName(customers);
},
TypeError,
);
});
Check programs/overloading.test.ts
running 4 tests from ./programs/overloading.test.ts
...
a union of tuples narrows like ordinary code ... FAILED (274µs)
ERRORS
a union of tuples narrows like ordinary code => ./programs/overloading.test.ts:105:6
error: AssertionError: Expected function to throw.
FAILURES
a union of tuples narrows like ordinary code => ./programs/overloading.test.ts:105:6
FAILED | 3 passed | 1 failed (2ms)
error: Test failed
No throw at all. The unchecked call falls into the one-argument branch, reads fullName off a Map that has no such property, and returns undefined, silently, which is the failure mode the checker was preventing. Correct the prediction:
Deno.test("a union of tuples narrows like ordinary code", () => {
function tupleFullName(
...args: [customer: Customer] | [map: Map<string, Customer>, id: string]
): string {
if (args.length === 2) {
const [map, id] = args;
const found = map.get(id);
if (found === undefined) throw new TypeError(`unknown id: ${id}`);
return found.fullName;
}
const [customer] = args;
return customer.fullName;
}
assertStrictEquals(tupleFullName(ada), "Ada Lovelace");
assertStrictEquals(tupleFullName(customers, "5678"), "Lars Croft");
// @ts-expect-error: Argument of type '[Map<string, Customer>]' is not assignable to parameter of type '[customer: Customer] | [map: Map<string, Customer>, id: string]'.
const wrong = tupleFullName(customers);
assertStrictEquals(wrong as unknown, undefined);
});
a union of tuples narrows like ordinary code ... ok (36µs)
The calls are checked exactly as well as the overloaded version, and inside the body args.length === 2 narrows args to the second tuple, so the destructuring on the next line has the right types with no assertion, the pattern from the destructuring page meeting the narrowing from the unions and narrowing page. The labels are documentation, behaving identically to unlabelled tuples and showing up in a tooltip, which is the only reason to write them. Two limits: the return type must be the same for every case, since there is one signature, and the narrowing is only as good as the discriminator you have, which here is the length. I would reach for this before overload signatures, because it keeps the function's type in one place, the body reads as normal code, and there is no second concept for a reader to know.
an interface can hold the signatures instead
Deno.test("an interface can hold the signatures instead", () => {
interface FullName {
(customer: Customer): string;
(map: Map<string, Customer>, id: string): string;
}
const viaInterface: FullName = fullName;
assertStrictEquals(viaInterface(ada), "Ada Lovelace");
assertStrictEquals(viaInterface(customers, "5678"), "Lars Croft");
});
an interface can hold the signatures instead ... ok (18µs)
Two call signatures in one interface, the same overload set expressed as a type rather than as declarations. The function types page introduces the single-signature version of this, and the interfaces and type aliases page explains why an interface can hold several while an alias holds one arrow. This is the form to reach for when the type is the deliverable: a callback parameter, a published type for somebody else's function, or a value you are assigning rather than declaring.
a string parameter can choose another parameter's type
Deno.test("a string parameter can choose another parameter's type", () => {
type ClickEvent = { x: number; y: number };
type KeyEvent = { key: string };
const handled: string[] = [];
function on(type: "click", listener: (event: ClickEvent) => void): void;
function on(type: "keypress", listener: (event: KeyEvent) => void): void;
function on(
type: string,
listener: (event: ClickEvent & KeyEvent) => void,
): void {
handled.push(type);
listener({ x: 1, y: 2, key: "a" });
}
on("click", (event) => {
assertStrictEquals(event.x, 1);
// @ts-expect-error: Property 'key' does not exist on type 'ClickEvent'.
assertStrictEquals(event.key, "a");
});
on("keypress", (event) => {
assertStrictEquals(event.key, "a");
});
assertStrictEquals(handled.join(","), "click,keypress");
});
a string parameter can choose another parameter's type ... ok (30µs)
This is the case that earns the feature, and no alternative does it as well. The literal value of the first argument selects the type of the second, so a callback written with no annotation gets the right event type, and reading event.key in the "click" handler is the shielded compile error, even though the test's implementation happens to supply it. Every addEventListener in every DOM type definition works this way, and so does every typed event emitter: if you are writing an API where a name selects a shape, this is the tool. The cost is in the implementation signature, which has to accept every combination, so it tends to end up with a union that is not quite right, or an intersection as above, or in real code an any. That signature is invisible to callers, which is the consolation, and it is also unchecked in the way that matters: the body is trusted rather than verified.
methods overload the same way
Deno.test("methods overload the same way", () => {
class Builder {
#text = "";
add(value: number): this;
add(value: boolean): this;
add(value: string): this;
add(value: number | boolean | string): this {
this.#text += String(value);
return this;
}
toString(): string {
return this.#text;
}
}
const built = new Builder().add("I can see ").add(3).add(" monkeys: ").add(
true,
);
assertStrictEquals(built.toString(), "I can see 3 monkeys: true");
const set = new Set(["a", "bb"]);
const copied: string[] = Array.from(set);
const mapped: number[] = Array.from(set, (item) => item.length);
assertStrictEquals(copied.join(","), "a,bb");
assertStrictEquals(mapped.join(","), "1,2");
});
methods overload the same way ... ok (52µs)
Same syntax inside a class body, with the return type this keeping the chain working in a subclass, the polymorphic this from the interfaces and type aliases page. This particular Builder is a good illustration and a bad design: three overloads that all forward to String(value) gain nothing over a single add(value: number | boolean | string), since every call is legal either way. Overloads pay off when the signatures differ in what they permit, not when they enumerate a union, and the standard library is full of the paying kind: Array.from has one signature returning T[] and another that takes a mapping function and returns U[], two shapes with two return types, which a single signature cannot express. You have used this without noticing, which is what a good overload feels like.
when not to
Deno.test("when not to", () => {
function nameOf(customer: Customer): string {
return customer.fullName;
}
function nameById(map: Map<string, Customer>, id: string): string {
const found = map.get(id);
if (found === undefined) throw new TypeError(`unknown id: ${id}`);
return found.fullName;
}
assertStrictEquals(nameOf(ada), "Ada Lovelace");
assertStrictEquals(nameById(customers, "5678"), "Lars Croft");
assertThrows(() => nameById(customers, "9999"), TypeError, "unknown id");
});
when not to ... ok (45µs)
Two names, two signatures, no overload set, no implementation signature, no instanceof check, and no step of this page explaining why a call that would work is refused. Each function is shorter than the branch it replaced, and the names say which one you want. The cases where overloading genuinely wins are narrower than they look: a name selects a type, as in the on("click", ...) case, which nothing else does; a parameter changes the return type, as in Array.from, which a union of tuples cannot express; and a published API you do not control the callers of, where adding a second name is a breaking change and an overload is not. Everything else is usually two functions.
The whole entry
Run the whole reference suite:
Check programs/any-unknown-never.test.ts
Check programs/arrays.test.ts
Check programs/assignment.test.ts
Check programs/async-functions.test.ts
Check programs/async-iteration.test.ts
Check programs/branching.test.ts
Check programs/buffers-and-views.test.ts
Check programs/classes.test.ts
Check programs/closures.test.ts
Check programs/conversion-and-coercion.test.ts
Check programs/dates-and-times.test.ts
Check programs/designing-error-types.test.ts
Check programs/destructuring.test.ts
Check programs/equality.test.ts
Check programs/errors-and-exceptions.test.ts
Check programs/function-types.test.ts
Check programs/functions.test.ts
Check programs/generators.test.ts
Check programs/interfaces-and-type-aliases.test.ts
Check programs/iterables-and-iterators.test.ts
Check programs/iterator-helpers.test.ts
Check programs/json.test.ts
Check programs/loops.test.ts
Check programs/maps.test.ts
Check programs/matching-and-replacing.test.ts
Check programs/module-specifiers.test.ts
Check programs/modules.test.ts
Check programs/mutating-arrays.test.ts
Check programs/nothing-twice.test.ts
Check programs/numbers.test.ts
Check programs/object-types.test.ts
Check programs/objects-as-dictionaries.test.ts
Check programs/objects.test.ts
Check programs/ordering-and-sorting.test.ts
Check programs/overloading.test.ts
Check programs/parameters-and-arguments.test.ts
Check programs/private-class-members.test.ts
Check programs/promise-combinators.test.ts
Check programs/promises.test.ts
Check programs/prototypes-and-inheritance.test.ts
Check programs/read-only.test.ts
Check programs/regular-expressions.test.ts
Check programs/scope-and-declarations.test.ts
Check programs/sentinels.test.ts
Check programs/sets.test.ts
Check programs/strings.test.ts
Check programs/subclassing.test.ts
Check programs/symbols.test.ts
Check programs/tagged-templates.test.ts
Check programs/text-and-characters.test.ts
Check programs/the-event-loop.test.ts
Check programs/the-value-of-this.test.ts
Check programs/transforming-arrays.test.ts
Check programs/truthiness.test.ts
Check programs/typed-arrays.test.ts
Check programs/unicode-in-patterns.test.ts
Check programs/unions-and-narrowing.test.ts
Check programs/values-and-references.test.ts
Check programs/weak-collections.test.ts
Check programs/what-a-type-is.test.ts
running 10 tests from ./programs/any-unknown-never.test.ts
...
running 13 tests from ./programs/arrays.test.ts
...
running 9 tests from ./programs/assignment.test.ts
...
running 10 tests from ./programs/async-functions.test.ts
...
running 11 tests from ./programs/async-iteration.test.ts
...
running 10 tests from ./programs/branching.test.ts
...
running 12 tests from ./programs/buffers-and-views.test.ts
...
running 11 tests from ./programs/classes.test.ts
...
running 6 tests from ./programs/closures.test.ts
...
running 11 tests from ./programs/conversion-and-coercion.test.ts
...
running 13 tests from ./programs/dates-and-times.test.ts
...
running 10 tests from ./programs/designing-error-types.test.ts
...
running 14 tests from ./programs/destructuring.test.ts
...
running 11 tests from ./programs/equality.test.ts
...
running 10 tests from ./programs/errors-and-exceptions.test.ts
...
running 12 tests from ./programs/function-types.test.ts
...
running 11 tests from ./programs/functions.test.ts
...
running 12 tests from ./programs/generators.test.ts
...
running 9 tests from ./programs/interfaces-and-type-aliases.test.ts
...
running 14 tests from ./programs/iterables-and-iterators.test.ts
...
running 12 tests from ./programs/iterator-helpers.test.ts
...
running 11 tests from ./programs/json.test.ts
...
running 14 tests from ./programs/loops.test.ts
...
running 15 tests from ./programs/maps.test.ts
...
running 15 tests from ./programs/matching-and-replacing.test.ts
...
running 6 tests from ./programs/module-specifiers.test.ts
...
running 12 tests from ./programs/modules.test.ts
...
running 10 tests from ./programs/mutating-arrays.test.ts
...
running 11 tests from ./programs/nothing-twice.test.ts
...
running 15 tests from ./programs/numbers.test.ts
...
running 15 tests from ./programs/object-types.test.ts
...
running 14 tests from ./programs/objects-as-dictionaries.test.ts
...
running 13 tests from ./programs/objects.test.ts
...
running 12 tests from ./programs/ordering-and-sorting.test.ts
...
running 8 tests from ./programs/overloading.test.ts
the problem overloading solves ... ok (374µs)
two signatures and one implementation ... ok (62µs)
the implementation signature is not one of the overloads ... ok (40µs)
a union of tuples narrows like ordinary code ... ok (33µs)
an interface can hold the signatures instead ... ok (16µs)
a string parameter can choose another parameter's type ... ok (33µs)
methods overload the same way ... ok (49µs)
when not to ... ok (47µs)
running 11 tests from ./programs/parameters-and-arguments.test.ts
...
running 11 tests from ./programs/private-class-members.test.ts
...
running 11 tests from ./programs/promise-combinators.test.ts
...
running 11 tests from ./programs/promises.test.ts
...
running 12 tests from ./programs/prototypes-and-inheritance.test.ts
...
running 12 tests from ./programs/read-only.test.ts
...
running 13 tests from ./programs/regular-expressions.test.ts
...
running 9 tests from ./programs/scope-and-declarations.test.ts
...
running 8 tests from ./programs/sentinels.test.ts
...
running 13 tests from ./programs/sets.test.ts
...
running 10 tests from ./programs/strings.test.ts
...
running 11 tests from ./programs/subclassing.test.ts
...
running 10 tests from ./programs/symbols.test.ts
...
running 8 tests from ./programs/tagged-templates.test.ts
...
running 10 tests from ./programs/text-and-characters.test.ts
...
running 9 tests from ./programs/the-event-loop.test.ts
...
running 10 tests from ./programs/the-value-of-this.test.ts
...
running 13 tests from ./programs/transforming-arrays.test.ts
...
running 9 tests from ./programs/truthiness.test.ts
...
running 14 tests from ./programs/typed-arrays.test.ts
...
running 11 tests from ./programs/unicode-in-patterns.test.ts
...
running 13 tests from ./programs/unions-and-narrowing.test.ts
...
running 13 tests from ./programs/values-and-references.test.ts
...
running 9 tests from ./programs/weak-collections.test.ts
...
running 7 tests from ./programs/what-a-type-is.test.ts
...
ok | 670 passed | 0 failed (1s)
Eight tests, and the practice is short. Try two names first, because most overloads exist because one name felt tidier, and the tidiness costs a concept, an unchecked implementation signature, and a class of error message people find confusing. Reach for a union of tuple types before overload signatures, since one signature and a body that narrows like ordinary code cover most of the rest. Use real overloads when a literal argument selects another parameter's type, or when the return type depends on the arguments, which is what the feature is for. Keep the implementation signature as tight as you can, and expect it to be looser than either overload, because it is invisible to callers and unverified against them, so it is where mistakes hide. And read TS2793 as instructions: the call would have succeeded against the implementation means you have two choices, narrow at the call site or publish a signature that accepts what you are passing.