Skip to content
aviral gupta

// B4.3 · ~30 min · Beginner

Function overloads

After this lesson you can write a function that callers may call in several ways, read the errors tsc gives for it, and tell when a union parameter is simpler.

Lesson 3 of 5 in B4 More on functions

You will be able to

  • Write overload signatures, and an implementation signature that is compatible with them
  • Read the errors of overloaded calls, knowing that callers see only the overload signatures
  • Prefer a union parameter to overloads when the return type does not depend on the argument
  1. Warm-up · Activity 1 of 7

    Warm-up from JavaScript: len is called with three kinds of value. What does this print?

    function len(x) {
      return x.length;
    }
    
    console.log(len("hello"), len([1, 2]), len(42));
  2. Predict · Activity 2 of 7

    Predict before you read on. makeDate has two overload signatures, then an implementation with two optional parameters. What does tsc report for the last line?

    function makeDate(timestamp: number): Date;
    function makeDate(m: number, d: number, y: number): Date;
    function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
      if (d !== undefined && y !== undefined) {
        return new Date(y, mOrTimestamp, d);
      } else {
        return new Date(mOrTimestamp);
      }
    }
    const d1 = makeDate(12345678);
    const d2 = makeDate(5, 5, 5);
    const d3 = makeDate(1, 3);
  3. Practice · Activity 3 of 7

    Fill in the parameter type of the implementation, so that it is compatible with both overloads and the program prints 499 [ 150, 25 ].

    function toCents(euros: number): number;
    function toCents(euros: number[]): number[];
    function toCents(euros: ____): number | number[] {
      if (Array.isArray(euros)) return euros.map((e) => Math.round(e * 100));
      return Math.round(euros * 100);
    }
    
    console.log(toCents(4.99), toCents([1.5, 0.25]));
    function toCents(euros: ): number | number[] {
  4. Practice · Activity 4 of 7

    pad has defaults for width and fill in its implementation. Which line does tsc reject?

    function pad(text: string): string;
    function pad(text: string, width: number, fill: string): string;
    function pad(text: string, width = 8, fill = " "): string {
      return text.padStart(width, fill);
    }
    
    pad("7");
    pad("7", 3);
    pad("7", 3, "0");
  5. Practice · Activity 5 of 7

    The second overload promises a boolean. What does tsc report?

    function fn(x: string): string;
    function fn(x: number): boolean;
    function fn(x: string | number) {
      return "oops";
    }
  6. Brain teaser · Activity 6 of 7

    Brain teaser. The implementation even handles a missing input. What does tsc report for this file?

    function greet(name: string): string;
    function greet(names: string[]): string;
    function greet(input?: string | string[]): string {
      if (input === undefined) return "Hello, nobody";
      return "Hello, " + (Array.isArray(input) ? input.join(" and ") : input);
    }
    
    console.log(greet(["Ada", "Grace"]));
    console.log(greet());
  7. Apply · Activity 7 of 7

    Mini-task. Write euro, which turns cents into text such as 4.50 EUR. Called with one number, it returns a string; called with an array of numbers, a string[]. Use two overload signatures and one implementation. Then write size, which returns the length of a string or of a string[]: one signature with a union parameter, because the return type is the same either way. Store euro(450) in a const of type string and euro([199, 2500]) in one of type string[]. Check it in the editor, or with npx tsc and node main.ts.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Dates, cents and lengths

makeDate is called with a timestamp or with year, month and day: the argument counts differ, so it needs overloads. toCents returns a number for a number and an array for an array: the return type depends on the argument, so it has overloads too. len returns a number either way, so one union parameter is enough. Run npx tsc, then node main.ts. Then add the line makeDate(2026, 10); and run tsc again.

main.ts

// One function, two ways to call it: overload signatures, then one implementation.
function makeDate(timestamp: number): Date;
function makeDate(year: number, month: number, day: number): Date;
function makeDate(yearOrTimestamp: number, month?: number, day?: number): Date {
  if (month !== undefined && day !== undefined) {
    return new Date(Date.UTC(yearOrTimestamp, month - 1, day));
  }
  return new Date(yearOrTimestamp);
}

// The return type follows the argument: a number gives a number, an array an array.
function toCents(euros: number): number;
function toCents(euros: number[]): number[];
function toCents(euros: number | number[]): number | number[] {
  if (Array.isArray(euros)) return euros.map((e) => Math.round(e * 100));
  return Math.round(euros * 100);
}

// The same return type either way: a union parameter is simpler.
function len(x: string | number[]): number {
  return x.length;
}

const day = (date: Date) => date.toISOString().slice(0, 10);
console.log(day(makeDate(0)), day(makeDate(2026, 10, 4)));

const one = toCents(4.99); // number
const many = toCents([1.5, 0.25]); // number[]
console.log(one + 1, many.join(" + "));

const input = Math.random() > 0.5 ? "hello" : [1, 2, 3, 4, 5];
console.log(len(input));

Run it with

npx tsc
node main.ts

Output

1970-01-01 2026-10-04
500 150 + 25
5
  • one is a number, so one + 1 type-checks; with only the implementation signature it would be number | number[].
  • makeDate(2026, 10) is error TS2575: no overload takes two arguments.
  • input is "hello" or an array of five numbers, so len prints 5 either way, and only a union parameter accepts it.
  • Date.UTC counts months from 0, hence month - 1 in the implementation.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

A duration with two overloads

duration(90) should give 1:30, and duration(2, 5), with minutes and seconds, 2:05. The two overloads are there, and tsc accepts the file: an implementation with fewer parameters counts as compatible. But it ignores the second argument, so duration(2, 5) gives 0:02. Change the implementation signature so that it receives both arguments, the second one optional, and work out the total seconds for either way of calling. Keep the overloads as they are.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    The implementation must accept what both overloads pass: one number, or two. Make the second parameter optional.

  2. Hint 2

    Name the parameters for what they may hold, for example (first: number, second?: number).

  3. Hint 3

    const total = second === undefined ? first : first * 60 + second; then compute minutes and seconds from total as before.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function duration(seconds: number): string;
export function duration(minutes: number, seconds: number): string;
export function duration(first: number, second?: number): string {
  const total = second === undefined ? first : first * 60 + second;
  const minutes = Math.floor(total / 60);
  return minutes + ":" + String(total % 60).padStart(2, "0");
}
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export function duration(seconds: number): string;
export function duration(minutes: number, seconds: number): string;
export function duration(total: number): string {
  const minutes = Math.floor(total / 60);
  return minutes + ":" + String(total % 60).padStart(2, "0");
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {duration} from './main.ts';

test('duration(90) is 1:30', () => {
  assert.equal(duration(90), '1:30', `duration(90) gave ${JSON.stringify(duration(90))}`);
});

test('duration(5) is 0:05', () => {
  assert.equal(duration(5), '0:05', `duration(5) gave ${JSON.stringify(duration(5))}`);
});

test('duration(2, 5) is 2:05', () => {
  assert.equal(duration(2, 5), '2:05', `duration(2, 5) gave ${JSON.stringify(duration(2, 5))}`);
});

test('duration(1, 75) carries the seconds over: 2:15', () => {
  assert.equal(duration(1, 75), '2:15', `duration(1, 75) gave ${JSON.stringify(duration(1, 75))}`);
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Exercise 2 of 2

From overloads to a union

wordCount counts the words in a string or in an array of lines. countAll passes a string | string[] on to it, and tsc reports error TS2769: No overload matches this call. Both overloads take one argument and return a number, so replace them with a single signature: keep the implementation, with its union parameter, and delete the two overload lines. Keep countAll as it is.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    TypeScript resolves a call to one overload, and input in countAll might be either kind of value.

  2. Hint 2

    The implementation signature already has the union parameter. Once the overloads are gone, it is the one callers see.

  3. Hint 3

    Delete the lines export function wordCount(text: string): number; and export function wordCount(lines: string[]): number;.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function wordCount(input: string | string[]): number {
  const text = Array.isArray(input) ? input.join(" ") : input;
  return text.split(" ").filter((word) => word !== "").length;
}

export function countAll(input: string | string[]): number {
  return wordCount(input);
}
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export function wordCount(text: string): number;
export function wordCount(lines: string[]): number;
export function wordCount(input: string | string[]): number {
  const text = Array.isArray(input) ? input.join(" ") : input;
  return text.split(" ").filter((word) => word !== "").length;
}

export function countAll(input: string | string[]): number {
  return wordCount(input);
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {wordCount, countAll} from './main.ts';

test('wordCount counts the words in a string', () => {
  assert.equal(wordCount('to be or not'), 4, `wordCount('to be or not') gave ${wordCount('to be or not')}`);
});

test('wordCount counts the words in all lines', () => {
  assert.equal(wordCount(['a b', 'c']), 3, `wordCount(['a b', 'c']) gave ${wordCount(['a b', 'c'])}`);
});

test('countAll works for a string and for lines', () => {
  assert.equal(countAll('one two'), 2, `countAll('one two') gave ${countAll('one two')}`);
  assert.equal(countAll(['one', 'two three']), 3, `countAll(['one', 'two three']) gave ${countAll(['one', 'two three'])}`);
});

test('an empty string has no words', () => {
  assert.equal(wordCount(''), 0, `wordCount('') gave ${wordCount('')}`);
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Common mistakes

Calling with the implementation's parameter list

function pad(text: string): string;
function pad(text: string, width: number, fill: string): string;
function pad(text: string, width = 8, fill = " "): string {
  return text.padStart(width, fill);
}

console.log(pad("7", 3));

What tsc or Node.js prints

main.ts(7,13): error TS2575: No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.

Why, and the fix

The defaults belong to the implementation signature, and callers cannot see it: only the overloads count. Add the missing way of calling as an overload, pad(text: string, width: number): string;, or, as nothing here changes the return type, drop the overloads and keep one signature with the defaults.

A single overload hides the wider implementation

function parse(text: string): number;
function parse(text: string | number): number {
  return Number(text);
}
console.log(parse(42));

What tsc or Node.js prints

main.ts(5,19): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

Why, and the fix

With one overload, callers see only parse(text: string), although the implementation accepts numbers too. The Handbook says to always write two or more overloads above the implementation. Here no overload is needed at all: delete the first line, and function parse(text: string | number): number is what callers see.

Overloads for a value that might be either

function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
  return x.length;
}

console.log(len(Math.random() > 0.5 ? "hello" : [0]));

What tsc or Node.js prints

main.ts(7,17): error TS2769: No overload matches this call.

Why, and the fix

The argument is a string or an array, and TypeScript resolves a call to a single overload; neither takes both. The lines after the message show what each overload rejected. As both overloads return a number, replace them with one signature: function len(x: any[] | string) { return x.length; }.

TypeScript in the browser: the TypeScript 6.0.3 compiler, Apache-2.0, then your browser’s own engine. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Overload signatures, then one implementation

Some JavaScript functions can be called in different ways. The Handbook's makeDate takes a timestamp, or a month, day and year. In TypeScript you list each way as an overload signature, a function header ending in ; with no body: function makeDate(timestamp: number): Date; and function makeDate(m: number, d: number, y: number): Date;. Then comes one implementation with a body, whose parameters cover every overload: function makeDate(mOrTimestamp: number, d?: number, y?: number): Date { … }. Overloads also let the return type follow the argument. With toCents(euros: number): number and toCents(euros: number[]): number[], toCents(4.99) is a number. One union signature would return number | number[], and toCents(4.99) + 1 would be error TS2365.

Callers see only the overloads

The Handbook: "The signature of the implementation is not visible from the outside." So makeDate(1, 3) is error TS2575: No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments., although d and y are optional in the implementation. With a single overload, function fn(x: string): void; function fn() {}, the call fn() is error TS2554: Expected 1 arguments, but got 0. So write two or more overloads. Each must be compatible with the implementation, or tsc reports error TS2394: This overload signature is not compatible with its implementation signature. An implementation with fewer parameters still counts as compatible (B4.1), even if it ignores an argument.

Prefer a union parameter

The Handbook's len has two overloads, len(s: string): number and len(arr: any[]): number. len("") and len([0]) are fine, but a value that might be either fails: len(Math.random() > 0.5 ? "hello" : [0]) is error TS2769: No overload matches this call. TypeScript resolves each call to a single overload, and neither one accepts both. Both overloads take one argument and return a number, so one signature does the job better: function len(x: any[] | string). Callers may pass either, with no implementation signature to get right. The Handbook's rule: "Always prefer parameters with union types instead of overloads when possible". Keep overloads for when the return type depends on the argument, or the argument counts differ, as in makeDate.

Sources

Last reviewed October 4, 2026