cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

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.

By the end, you can
  • 01
    Write overload signaturesList the ways a function can be called, each with its own return type, above one implementation.
  • 02
    Read how a call is matchedPredict which signature TypeScript picks for a call, and read No overload matches this call in plain words.
  • 03
    Choose 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:

One price or a list of pricesPop out in the code editor (opens in a new tab)JavaScript
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:

A union parameter loses track of the answer (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

The same function with two overloadsPop out in the code editor (opens in a new tab)TypeScript
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.

Two calls. parsePrice("4") matches the signature (input: string): number and gives a number. parsePrice(["3", "4"]) matches (input: string[]): number[] and gives number[].the signature it matchesthe type it gives backparsePrice("4")(input: string): numbernumberparsePrice(["3", "4"])(input: string[]): number[]number[]
Each call is matched with one signature. A string matches the first, so price is a number. A list of strings matches the second, so prices is a number[].
Real-life analogyThe ticket desk's board

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 string gives a number; a string[] gives a number[]
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
The main idea

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:

Playground: type the argument
Which signature does the call match?Pop out in the code editor (opens in a new tab)TypeScript
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));
Line 7 passes"4"
What happens
TypeScriptNo errors
Type of resultnumber
When it runsprints 4
Try it yourself

A string matches the first signature, (input: string): number, so line 8 shows const result: number. It prints 4.

The code is checked by the real TypeScript compiler each time you type. Only line 7 changes. Reset puts "4" back.

"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:

What the browser gets: one plain functionPop out in the code editor (opens in a new tab)JavaScript
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:

The implementation's optional name is hidden (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

Left, a window titled callers see, holding the two overload signatures. Right, the implementation signature (input: string | string[]), which callers can't see and which is the only signature the body is checked against.callers see(input: string): number(input: string[]): number[]the overloadsthe implementation(input: string | string[])hidden from callerschecks only the body
Callers see the two overloads, and a call must match one of them. The implementation signature is hidden. TypeScript checks the body only against it.

Second, the implementation signature must cover every overload:

An implementation that only takes a string (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

Swapped branches that TypeScript can't catchPop out in the code editor (opens in a new tab)TypeScript
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:

Overloads turn away a union argument (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

One union parameter insteadPop out in the code editor (opens in a new tab)TypeScript
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.

Left, two overloads: the value query of type string | string[] meets one signature that takes only string and one that takes only string[], and neither takes both. Right, one union parameter: the same value fits items: string | string[].Two overloadsquery: string | string[]stringstring[]neither takes bothOne union parameterquery: string | string[]items: string | string[]takes both
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:

Playground: add a signature for the union
parsePrice called with a unionPop out in the code editor (opens in a new tab)TypeScript
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"])));
Line 12: price isnumber
What happens
TypeScript1 error: line 9
Try it yourself

readSearch 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.

TypeScript checks the code for real for each choice. The buttons change only lines 1 to 3, and the code prints [3,4] every time. Reset goes back to Two overloads.

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.

Overloads and union parameters
QuestionOverloadsA 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:

Overloads that differ by one last parameter (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

One signature with an optional parameterPop out in the code editor (opens in a new tab)TypeScript
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:

The call parsePrice("4") and three signatures in order. Line 1, (input: string | string[]): number | number[], fits, so it is used. Lines 2 and 3 are never reached.parsePrice("4")1. (input: string | string[]): number | number[]2. (input: string): number3. (input: string[]): number[]fits: usednever reachednever reached
TypeScript tries the signatures from the top and stops at the first that fits. With the union signature on line 1, a string fits it, so 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 to overload, and what to write instead
When your overloads…Write insteadExample
Differ only in one parameter's type, and return the same typeOne parameter with a union type.countItems(items: string | string[]): number
Differ only by leaving off the last parameters, and return the same typeAn optional parameter.padding(vertical: number, horizontal?: number): string
Return a different type for different argumentsOverloads. This is what they're for.parsePrice("4") is a number, parsePrice(["4"]) is a number[].
Include a general one that takes a unionKeep 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:

createElement picks a signature from the tag namePop out in the code editor (opens in a new tab)TypeScript
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:

Error or fine?
  • parsePrice("4");
  • parsePrice(["3", "4"]);
  • parsePrice([]);
  • parsePrice(String(4));
  • parsePrice(4);
  • parsePrice([3, 4]);
  • parsePrice();
  • parsePrice("4", "5");
Try it yourself
0 of 8 correct

Each call uses the two parsePrice signatures: (input: string): number and (input: string[]): number[]. Sort it by what TypeScript says.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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.

Exercise 1 · Warm-upRead which overload a call matches

What type does TypeScript give prices? Guess first. Then point at prices, or press Edit & run and add a // ^? line under it.

Starter codePop out in the code editor (opens in a new tab)TypeScript
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"]);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the call no overload takes

    Three calls to an overloaded function. Which line will TypeScript mark?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    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");

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeAdd the missing overload

      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?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      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]));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFix an implementation that's too narrow

        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?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        function toCents(price: number): number;function toCents(prices: number[]): number[];function toCents(price: number) {  return Math.round(price * 100);} console.log(toCents(4.5));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeReplace overloads with a union

          Line 8 gets No overload matches this call. Fix it by removing code, not adding it. What does the fixed code print?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          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"]));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengePut the general overload last

            Line 9 gets an error, though toLabel(4) really returns text. Reorder the overloads so it works. What type does label get then?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            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());

            Answer, then press Check. Spacing and letter case don’t matter.

              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.

              The Function overloads quiz · 8 questionsScore: first tries count
              1. Question 1 of 8Which signatures can callers of an overloaded function use?

                Choose an answer to see the explanation.

              2. Question 2 of 8What type does TypeScript give prices in this code?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                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"]);

                Choose an answer to see the explanation.

              3. Question 3 of 8Which of the three parsePrice calls does TypeScript flag?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function 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.

              4. Question 4 of 8What happens to the overload lines when the code runs?

                Choose an answer to see the explanation.

              5. Question 5 of 8TypeScript reports no error in this code. What happens when it runs?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function 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.

              6. Question 6 of 8The union overload comes first here. What type does price get?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function 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.

              7. Question 7 of 8Why does TypeScript reject welcome() on line 6?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function welcome(name: string): string;
                function welcome(name?: string) {
                  return "Hello, " + (name ?? "guest");
                }
                
                console.log(welcome());

                Choose an answer to see the explanation.

              8. 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.

              CompleteFrontend Clear concepts. Working examples.