Function overloads
Give one function several call signatures so each way of calling it returns its own type, and learn when a union or an optional parameter is simpler.
- 01Write overload signaturesList the ways a function can be called, each with its own return type, above one implementation.
- 02Read how a call is matchedPredict which signature TypeScript picks for a call, and read
No overload matches this callin plain words. - 03Choose overloads only when neededTell when a union type or an optional parameter does the job better, and order overloads so none is hidden.
One function, two kinds of answer
Some JavaScript functions give back a different kind of value depending on what you pass. This one reads prices typed into a form. Give it one price as text, and it gives back a number. Give it a list of prices, and it gives back a list of numbers:
function parsePrice(input) {
return Array.isArray(input) ? input.map(Number) : Number(input);
}
console.log(parsePrice("4"));
console.log(parsePrice(["3", "4"]).join(", "));Line 2 checks whether input is a list with Array.isArray. If it is, it turns every item into a number with map; if not, it turns the one text into a number. Line 5 prints 4, and line 6 prints 3, 4.
How would you type it? The Union types lesson suggests string | string[] for the parameter. That works for the input, but look at what comes back:
function parsePrice(input: string | string[]) { return Array.isArray(input) ? input.map(Number) : Number(input);} const price = parsePrice("4");// ^?console.log(price.toFixed(2));Line 1 takes a string or a list of strings. Line 5 passes "4", a string, so you know the answer is a number. But the query on line 6 shows const price: number | number[]: TypeScript only knows the function returns one or the other. So line 7 gets an error, on purpose here: Property 'toFixed' does not exist on type 'number | number[]'. It means "this might be a list, and lists don't have toFixed". The code runs fine and prints 4.00; the type is just too vague.
Overloads fix this. You list each way the function can be called, with the type it gives back for that way:
function parsePrice(input: string): number;function parsePrice(input: string[]): number[];function parsePrice(input: string | string[]) { return Array.isArray(input) ? input.map(Number) : Number(input);} const price = parsePrice("4");// ^?const prices = parsePrice(["3", "4"]);// ^?console.log(price.toFixed(2));console.log(prices.length);Lines 1 and 2 are the two ways to call it: with a string, you get a number; with a list of strings, a list of numbers. Lines 3 to 5 are the function itself. Now the query on line 8 shows const price: number, and the query on line 10 shows const prices: number[]. Line 11 prints 4.00 with no error, and line 12 prints 2.
price is a number. A list of strings matches the second, so prices is a number[].A cinema's ticket desk has a board: "One person: give your name, get one ticket. A group: give a list of names, get a list of tickets." The clerk behind the desk handles both with the same notes, but you only ever order from the board. If you ask for something that isn't on it, the clerk turns you away.
- In real life: The board lists the ways you can order
- In TypeScript: The overload signatures on lines 1 and 2
- In real life: Give one name, get one ticket. Give a list of names, get a list of tickets
- In TypeScript: A
stringgives anumber; astring[]gives anumber[] - In real life: The clerk's own notes, which you never see
- In TypeScript: The implementation signature on line 3
- In real life: An order that isn't on the board is turned away
- In TypeScript:
No overload matches this call
Function overloads list the different ways one function can be called, each with its own parameter types and return type. TypeScript matches every call with one of them, so the result gets the precise type for that call. One function body below them does the work.
Overload signatures
A signature is a function's parameters and return type, without its body, like (input: string): number. An overloaded function is written in two parts:
- Overload signatures: usually two or more lines that start like a function but have no body. Each ends with a
;, and you write its return type. - The implementation: the function with a body, written right after them, with the same name. Its own signature has to cover every overload.
When you call the function, TypeScript tries the overload signatures from the top and uses the first one that fits. Type a different argument on line 7 and watch which signature it picks:
function parsePrice(input: string): number;function parsePrice(input: string[]): number[];function parsePrice(input: string | string[]) { return Array.isArray(input) ? input.map(Number) : Number(input);} const result = parsePrice("4");// ^?console.log(JSON.stringify(result));"4"No errorsnumberprints 4A string matches the first signature, (input: string): number, so line 8 shows const result: number. It prints 4.
"4" fits the first signature, so result is a number. ["3", "4"] and [] fit the second, so it's a number[]. For 4 or [3, 4], TypeScript says No overload matches this call. Read the lines under it in the list: Overload 1 of 2, '(input: string): number', gave the following error, then the reason, and the same for overload 2. It shows how each signature turned the argument down. For two arguments, it says Expected 1 arguments, but got 2.
Overload signatures are types, so they disappear when the types are removed. Here is what the browser gets for the example above:
function parsePrice(input) {
return Array.isArray(input) ? input.map(Number) : Number(input);
}
const price = parsePrice("4");
const prices = parsePrice(["3", "4"]);
console.log(price.toFixed(2));
console.log(prices.length);Lines 1 and 2 of the TypeScript are gone, and so are the types. There is one function, and its own code, the Array.isArray check on line 2, decides what to do. JavaScript doesn't pick between bodies when it runs; overloads only tell TypeScript what each call gives back.
The implementation signature
The signature on the function with the body is called the implementation signature. It has three rules that surprise people. First, callers can't see it:
function welcome(name: string): string;function welcome(name?: string) { return "Hello, " + (name ?? "guest");} console.log(welcome("Asha"));console.log(welcome());Line 1 is the only overload: welcome takes a name. Line 2's implementation makes the name optional with name?, and line 3 uses "guest" when it's missing. Line 6 prints Hello, Asha. Line 7 calls welcome() with no name, and TypeScript says Expected 1 arguments, but got 0, on purpose here. Callers only see line 1. If you want a call with no name, write an overload for it. Run it anyway, and line 7 prints Hello, guest.
Second, the implementation signature must cover every overload:
function parsePrice(input: string): number;function parsePrice(input: string[]): number[];function parsePrice(input: string) { return Number(input);}Line 2 promises callers they can pass a list of strings, but line 3's implementation only takes a string. TypeScript marks line 2: This overload signature is not compatible with its implementation signature. It means "the function below can't take what this line promises". Fix it by widening line 3 to input: string | string[], as in the overview.
Third, TypeScript checks the body only against the implementation signature, not against each overload:
function parsePrice(input: string): number;function parsePrice(input: string[]): number[];function parsePrice(input: string | string[]) { return Array.isArray(input) ? Number(input[0]) : [Number(input)];} const price = parsePrice("4");// ^?console.log(price.toFixed(2));Line 4 has the two branches swapped: a list gives back one number, and a string gives back a list. Both are allowed by the implementation's return type, number | number[], so there are no errors. The query on line 8 still trusts line 1 and says const price: number. When it runs, price is really [4], and line 9 throws a TypeError. Overloads are promises you make; keep the body honest, and test it.
Overloads vs unions
TypeScript matches a call with one overload. That matters when the argument itself is a union, like a search that may hold one word or several:
function countItems(items: string): number;function countItems(items: string[]): number;function countItems(items: string | string[]) { return items.length;} function countSearch(query: string | string[]) { return countItems(query);}console.log(countSearch(["tea", "cake"]));Lines 1 and 2 take a string or a list, and both give back a number. Line 7's query may be either, so on line 8 neither overload fits the whole thing, and TypeScript says No overload matches this call. When it runs, line 10 prints 2, because the body handles both. The overloads add nothing here: the return type is the same either way. One union parameter is all you need:
function countItems(items: string | string[]) { return items.length;} function countSearch(query: string | string[]) { return countItems(query);}console.log(countSearch(["tea", "cake"]));Line 1 takes string | string[] and returns a number. Line 6 passes query with no error, and line 8 prints 2.
query may be a string or a list. Each overload takes only one of those, so the call on line 8 matches neither. A union parameter takes both.parsePrice is different: its return type does depend on the argument, so you keep the overloads. To let a union through as well, add a third signature that takes the union. Where you put it matters. Try the three choices:
function parsePrice(input: string): number;function parsePrice(input: string[]): number[];// no overload for string | string[] yetfunction parsePrice(input: string | string[]) { return Array.isArray(input) ? input.map(Number) : Number(input);} function readSearch(query: string | string[]) { return parsePrice(query);} const price = parsePrice("4");// ^?console.log(JSON.stringify(readSearch(["3", "4"])));number1 error: line 9readSearch passes a string | string[], and neither signature takes that whole union, so line 9 gets No overload matches this call. Line 12 still matches line 1: const price: number.
With Two overloads, line 9 gets an error. Union overload last fixes it and keeps price a number. Union overload first also has no errors, but every string now matches line 1, so price becomes number | number[] again, as if you had no overloads at all.
The table below compares the two tools. Read each row as a question, then the answer for each.
| Question | Overloads | A union parameter |
|---|---|---|
| How many signatures do callers see? | One per way of calling, like one for string and one for string[]. | One. |
| Can the return type depend on the argument? | Yes: "4" gives a number, ["4"] gives a number[]. | No. Every call gets the same return type, like number | number[]. |
Can a caller pass a string | string[] value? | Only if one overload takes that whole union. | Yes. |
| Is the body checked against each way of calling? | No. Only against the hidden implementation signature. | Yes. There is only one signature to check. |
When not to overload
Overloads are worth it when the return type depends on the arguments. Otherwise a plain signature is shorter and easier to call. Here are two overloads that differ only by a last parameter:
function padding(all: number): string;function padding(vertical: number, horizontal: number): string;function padding(vertical: number, horizontal?: number) { return horizontal === undefined ? vertical + "px" : vertical + "px " + horizontal + "px";} function cardPadding(gap?: number) { return padding(8, gap);}Line 1 makes CSS padding from one size, and line 2 from two. Line 7's cardPadding has an optional gap, so gap is number | undefined. Line 8 passes two arguments, so only line 2 can match, and it needs a number: Argument of type 'number | undefined' is not assignable to parameter of type 'number'. Both overloads return a string, so an optional parameter says the same thing in one line:
function padding(vertical: number, horizontal?: number) { return horizontal === undefined ? vertical + "px" : vertical + "px " + horizontal + "px";} function cardPadding(gap?: number) { return padding(8, gap);}console.log(cardPadding());console.log(cardPadding(16));Line 1 makes horizontal optional, and line 6 passes gap with no error. Line 8 prints 8px and line 9 prints 8px 16px.
And when you do overload, order matters, because TypeScript stops at the first signature that fits. Put general signatures, like one that takes a union, after the precise ones:
price is number | number[], and the precise line 2 is never reached.The table below sums up the choices. Read each row as: when your overloads look like this, write this instead.
| When your overloads… | Write instead | Example |
|---|---|---|
| Differ only in one parameter's type, and return the same type | One parameter with a union type. | countItems(items: string | string[]): number |
| Differ only by leaving off the last parameters, and return the same type | An optional parameter. | padding(vertical: number, horizontal?: number): string |
| Return a different type for different arguments | Overloads. This is what they're for. | parsePrice("4") is a number, parsePrice(["4"]) is a number[]. |
| Include a general one that takes a union | Keep it, but put it last. | parsePrice(input: string | string[]): number | number[] after the other two. |
Overloads you already use: createElement
You've used overloaded functions already. The browser's types give document.createElement three signatures. The first takes any tag name the browser knows, like "input", and returns that exact kind of element. The second does the same for a few old tag names. The last, general one takes any other name:
const input = document.createElement("input");// ^?const banner = document.createElement("div");// ^?const widget = document.createElement("cart-widget");// ^?input.value = "2";console.log(input.value, banner.tagName, widget.tagName);The query on line 2 shows const input: HTMLInputElement, so line 7 can set input.value. Line 4 shows HTMLDivElement. Line 5 asks for cart-widget, a name the browser doesn't know (a page can define its own elements like this), so it falls through to the general signature and line 6 shows plain HTMLElement. Line 8 prints 2 DIV CART-WIDGET. In your editor, pointing at createElement shows the signature it matched and (+2 overloads).
That's the pattern to copy: precise signatures first, so callers get exact types, and a general one last, so nothing reasonable is turned away. Now sort some calls to the two-signature parsePrice from the start of the lesson:
parsePrice("4");parsePrice(["3", "4"]);parsePrice([]);parsePrice(String(4));parsePrice(4);parsePrice([3, 4]);parsePrice();parsePrice("4", "5");
Each call uses the two parsePrice signatures: (input: string): number and (input: string[]): number[]. Sort it by what TypeScript says.
Common mix-ups
- "Overloads let me write a separate body for each way of calling." No. There is one body. The overload lines are removed before the code runs, and your own code inside the body tells the cases apart.
- "Callers can use the implementation signature too." No. It's hidden. Every call must match one of the overloads above it.
- "TypeScript checks that each branch returns what its overload promises." No. It checks the body only against the implementation signature, so swapped branches get no error.
- "A value of type
string | string[]can call a function with a string overload and a list overload." No. A call must fit one overload. Add a signature that takes the union, last, or use a union parameter. - "The order of overloads doesn't matter." No. TypeScript uses the first one that fits, so a general one at the top hides the precise ones.
- "More overloads make a function easier to use." No. If the return type is the same, a union or an optional parameter is simpler for callers.
Practice exercises
Each exercise is TypeScript. When you fix code, press Edit & run on the starter code: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.
What type does TypeScript give prices? Guess first. Then point at prices, or press Edit & run and add a // ^? line under it.
function parsePrice(input: string): number;function parsePrice(input: string[]): number[];function parsePrice(input: string | string[]) { return Array.isArray(input) ? input.map(Number) : Number(input);} const prices = parsePrice(["3", "4", "5"]);function parsePrice(input: string): number;function parsePrice(input: string[]): number[];function parsePrice(input: string | string[]) { return Array.isArray(input) ? input.map(Number) : Number(input);} const prices = parsePrice(["3", "4", "5"]);// ^?A list of strings doesn't fit input: string, so TypeScript uses the second signature, which returns number[]. The query on line 8 shows const prices: number[].
Three calls to an overloaded function. Which line will TypeScript mark?
function formatPrice(price: number): string;
function formatPrice(prices: number[]): string[];
function formatPrice(price: number | number[]) {
return Array.isArray(price) ? price.map((one) => "$" + one) : "$" + price;
}
const tea = formatPrice(3);
const cart = formatPrice([3, 4]);
const cake = formatPrice("4");function formatPrice(price: number): string;function formatPrice(prices: number[]): string[];function formatPrice(price: number | number[]) { return Array.isArray(price) ? price.map((one) => "$" + one) : "$" + price;} const tea = formatPrice(3);const cart = formatPrice([3, 4]);const cake = formatPrice("4"); // line 9: text fits neither signatureLine 9 passes the text "4". It fits neither price: number nor prices: number[], so TypeScript says No overload matches this call.
Line 7 gets an error, though the body handles a list fine. Add an overload so the call works. What type does formatPrice([3, 4]) give back then?
function formatPrice(price: number): string;function formatPrice(price: number | number[]) { return Array.isArray(price) ? price.map((one) => "$" + one) : "$" + price;} console.log(formatPrice(4));console.log(formatPrice([3, 4]));function formatPrice(price: number): string;function formatPrice(prices: number[]): string[];function formatPrice(price: number | number[]) { return Array.isArray(price) ? price.map((one) => "$" + one) : "$" + price;} console.log(formatPrice(4));const labels = formatPrice([3, 4]);// ^?console.log(labels.join(", "));With only one overload, a list doesn't fit price: number. The new line 2 says a list of numbers gives back a list of strings, so the call works and returns string[], which has join. It prints $4, then $3, $4.
TypeScript marks line 2. Fix the implementation so it handles both overloads, then add console.log(toCents([3, 4.25]).join(", "));. What does it print?
function toCents(price: number): number;function toCents(prices: number[]): number[];function toCents(price: number) { return Math.round(price * 100);} console.log(toCents(4.5));function toCents(price: number): number;function toCents(prices: number[]): number[];function toCents(price: number | number[]) { return Array.isArray(price) ? price.map((one) => Math.round(one * 100)) : Math.round(price * 100);} console.log(toCents(4.5));console.log(toCents([3, 4.25]).join(", "));The implementation signature must cover every overload. With number | number[] it covers both, and the Array.isArray check turns each price into cents. toCents([3, 4.25]) gives [300, 425], printed as 300, 425.
Line 8 gets No overload matches this call. Fix it by removing code, not adding it. What does the fixed code print?
function countItems(items: string): number;function countItems(items: string[]): number;function countItems(items: string | string[]) { return items.length;} function countSearch(query: string | string[]) { return countItems(query);}console.log(countSearch(["tea", "cake"]));function countItems(items: string | string[]) { return items.length;} function countSearch(query: string | string[]) { return countItems(query);}console.log(countSearch(["tea", "cake"]));Without the overloads, the union parameter is what callers see, so countItems(query) fits. ["tea", "cake"] has two items, so it prints 2.
Line 9 gets an error, though toLabel(4) really returns text. Reorder the overloads so it works. What type does label get then?
function toLabel(price: number | number[]): string | string[];function toLabel(price: number): string;function toLabel(prices: number[]): string[];function toLabel(price: number | number[]) { return Array.isArray(price) ? price.map((one) => "$" + one) : "$" + price;} const label = toLabel(4);console.log(label.toUpperCase());function toLabel(price: number): string;function toLabel(prices: number[]): string[];function toLabel(price: number | number[]): string | string[];function toLabel(price: number | number[]) { return Array.isArray(price) ? price.map((one) => "$" + one) : "$" + price;} const label = toLabel(4);// ^?console.log(label.toUpperCase());With the union signature last, toLabel(4) matches price: number first, so label is a string and toUpperCase is allowed. It prints $4.
Check your understanding
For each question, find the signature a call matches, from the top. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 8Which signatures can callers of an overloaded function use?
Choose an answer to see the explanation.
Question 2 of 8What type does TypeScript give
pricesin this code?Read the code, then predictfunction parsePrice(input: string): number; function parsePrice(input: string[]): number[]; function parsePrice(input: string | string[]) { return Array.isArray(input) ? input.map(Number) : Number(input); } const prices = parsePrice(["3"]);Choose an answer to see the explanation.
Question 3 of 8Which of the three parsePrice calls does TypeScript flag?
Read the code, then predictfunction parsePrice(input: string): number; function parsePrice(input: string[]): number[]; function parsePrice(input: string | string[]) { return Array.isArray(input) ? input.map(Number) : Number(input); } parsePrice("4"); parsePrice([]); parsePrice(4);Choose an answer to see the explanation.
Question 4 of 8What happens to the overload lines when the code runs?
Choose an answer to see the explanation.
Question 5 of 8TypeScript reports no error in this code. What happens when it runs?
Read the code, then predictfunction parsePrice(input: string): number; function parsePrice(input: string[]): number[]; function parsePrice(input: string | string[]) { return Array.isArray(input) ? Number(input[0]) : [Number(input)]; } console.log(parsePrice("4").toFixed(2));Choose an answer to see the explanation.
Question 6 of 8The union overload comes first here. What type does
priceget?Read the code, then predictfunction parsePrice(input: string | string[]): number | number[]; function parsePrice(input: string): number; function parsePrice(input: string[]): number[]; function parsePrice(input: string | string[]) { return Array.isArray(input) ? input.map(Number) : Number(input); } const price = parsePrice("4");Choose an answer to see the explanation.
Question 7 of 8Why does TypeScript reject
welcome()on line 6?Read the code, then predictfunction welcome(name: string): string; function welcome(name?: string) { return "Hello, " + (name ?? "guest"); } console.log(welcome());Choose an answer to see the explanation.
Question 8 of 8countItems returns a number for a string and for a list. Which way of typing it is better?
Choose an answer to see the explanation.
Key takeaways
- Overload signatures list the ways a function can be called, each with its own return type, above one implementation.
- TypeScript tries the overloads from the top and uses the first that fits. If none fits, it says
No overload matches this call. - The implementation signature is hidden from callers, must cover every overload, and is the only thing the body is checked against.
- Overloads are removed with the types. The browser runs one function, and your code tells the cases apart.
- A call matches one overload, so a union argument needs a union signature, placed last, or a union parameter.
- Overload only when the return type depends on the arguments. Otherwise use a union or an optional parameter.
Remember the one-liner.
Overloads give each way of calling a function its own return type; reach for them only when the answer's type depends on the question.
Coming next: this in TypeScript. It shows how to say what this is inside a function, and how TypeScript catches a method called without its object.