Skip to content
aviral gupta

// B4.2 · ~30 min · Beginner

Optional, default and rest parameters

After this lesson you can write functions that take optional, default and any number of arguments, spread arrays into calls, and destructure a typed parameter.

Lesson 2 of 5 in B4 More on functions

You will be able to

  • Make a parameter optional with ? or a default, and handle undefined in the body
  • Collect arguments with a rest parameter, and spread arrays and tuples into calls
  • Annotate a destructured parameter, and type callbacks without needless optional parameters
  1. Warm-up · Activity 1 of 7

    Warm-up from JavaScript: name has a default value. What does this print?

    function greet(name = "guest") {
      return "Hello, " + name;
    }
    
    console.log(greet(), greet(undefined), greet(null));
  2. Predict · Activity 2 of 7

    Predict before you read on. name is optional, and greet() leaves it out. What does tsc report?

    function greet(name?: string): string {
      return "Hello, " + name.toUpperCase();
    }
    
    console.log(greet());
  3. Practice · Activity 3 of 7

    Fill in what turns values into a rest parameter, so that sum takes any number of numbers and the program prints 9.5.

    function sum(____values: number[]): number {
      let total = 0;
      for (const v of values) total += v;
      return total;
    }
    
    console.log(sum(3, 4.5, 2));
    function sum(values: number[]): number {
  4. Practice · Activity 4 of 7

    Which type does width have inside the function?

    function pad(text: string, width?: number): string {
      // what is width here?
      return text.padStart(width ?? 8);
    }
  5. Practice · Activity 5 of 7

    point takes exactly two numbers. Which line does tsc reject?

    function point(x: number, y: number): string {
      return "(" + x + ", " + y + ")";
    }
    
    const a = [3, 4];
    const b = [3, 4] as const;
    console.log(point(...a));
    console.log(point(...b));
  6. Brain teaser · Activity 6 of 7

    Brain teaser. tsc accepts this file. Which arguments does the default replace, and what does node main.ts print?

    function repeat(text: string, times = 2): string {
      return text.repeat(times);
    }
    
    const counts = [undefined, 0, 3];
    console.log(counts.map((n) => "[" + repeat("ab", n) + "]").join(" "));
  7. Apply · Activity 7 of 7

    Mini-task. An Order has an id, a list of item prices and an optional note. Write total(...prices), which adds any number of prices. Then write describe, which destructures { id, items, note } from an Order in its parameter list and takes a currency that defaults to "EUR". It returns text such as #1: 9.75 EUR, and adds the note in brackets when there is one. Use total(...items) inside. 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

A cafe receipt

euro has a default currency, line destructures an Item whose quantity is optional, printReceipt and sum take any number of arguments, and thanks has an optional name. The last two lines spread a tuple made with as const into a function with two fixed parameters. Run npx tsc, then node main.ts. Then remove as const and run tsc again.

main.ts

// A receipt: optional, default and rest parameters, and a destructured parameter.
type Item = { name: string; price: number; quantity?: number };

function euro(cents: number, symbol = "EUR"): string {
  return (cents / 100).toFixed(2) + " " + symbol; // symbol is a string, from its default
}

function line({ name, price, quantity }: Item): string {
  const count = quantity ?? 1; // quantity is number | undefined here
  return (count + " x " + name).padEnd(16) + euro(price * count);
}

function sum(...amounts: number[]): number {
  let total = 0;
  for (const amount of amounts) total += amount;
  return total;
}

function printReceipt(title: string, ...items: Item[]): void {
  console.log(title);
  for (const item of items) console.log("  " + line(item));
  const cents = items.map((item) => item.price * (item.quantity ?? 1));
  console.log("  " + "total".padEnd(16) + euro(sum(...cents)));
}

function thanks(name?: string): string {
  return name === undefined ? "Thank you!" : "Thank you, " + name + "!";
}

function clock(hours: number, minutes: number): string {
  return String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0");
}

const order: Item[] = [
  { name: "coffee", price: 320, quantity: 2 },
  { name: "croissant", price: 245 },
];
printReceipt("Cafe Mitte", ...order, { name: "water", price: 150 });
console.log(thanks());
console.log(thanks("Ada"));

const opens = [7, 30] as const; // a tuple of exactly two numbers
console.log("Open daily from " + clock(...opens));

Run it with

npx tsc
node main.ts

Output

Cafe Mitte
  2 x coffee      6.40 EUR
  1 x croissant   2.45 EUR
  1 x water       1.50 EUR
  total           10.35 EUR
Thank you!
Thank you, Ada!
Open daily from 07:30
  • symbol has no annotation: its default, "EUR", makes it a string, and every call here leaves it out.
  • quantity is optional in Item, so after destructuring it is number | undefined, and ?? 1 gives the count.
  • ...order spreads an array into the rest parameter items, and another item may follow it in the same call.
  • Without as const, opens is number[] and clock(...opens) is error TS2556.
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

Defaults for a price formatter

formatPrice(1250) should give 12.50 EUR, but all three parameters are required, so tsc reports error TS2554: Expected 3 arguments, but got 1. Give currency the default "EUR" and decimals the default 2, without annotations. Then add a fourth, optional parameter label: when it is given, the result starts with the label and a colon, as in Total: 12.50 EUR.

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

    A default goes after the parameter name with =, and TypeScript infers the type from it: currency = "EUR".

  2. Hint 2

    An optional parameter has a ? after its name and must come after the required ones: label?: string.

  3. Hint 3

    Inside, label may be undefined: return label === undefined ? text : label + ": " + text;

Show a solution

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

export function formatPrice(cents: number, currency = "EUR", decimals = 2, label?: string): string {
  const text = (cents / 100).toFixed(decimals) + " " + currency;
  return label === undefined ? text : label + ": " + text;
}

console.log(formatPrice(1250));
console.log(formatPrice(1999, "USD", 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 formatPrice(cents: number, currency: string, decimals: number): string {
  return (cents / 100).toFixed(decimals) + " " + currency;
}

console.log(formatPrice(1250));
console.log(formatPrice(1999, "USD", 0));

main.test.ts

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

test('formatPrice(1250) uses EUR and two decimals', () => {
  assert.equal(formatPrice(1250), '12.50 EUR', `formatPrice(1250) gave ${JSON.stringify(formatPrice(1250))}`);
});

test('a given currency and number of decimals are used', () => {
  assert.equal(formatPrice(1999, 'USD', 0), '20 USD', `formatPrice(1999, 'USD', 0) gave ${JSON.stringify(formatPrice(1999, 'USD', 0))}`);
});

test('undefined as the currency gives the default', () => {
  assert.equal(formatPrice(1250, undefined, 1), '12.5 EUR', `formatPrice(1250, undefined, 1) gave ${JSON.stringify(formatPrice(1250, undefined, 1))}`);
});

test('a label goes in front, with a colon', () => {
  assert.equal(formatPrice(1250, 'EUR', 2, 'Total'), 'Total: 12.50 EUR', `formatPrice(1250, 'EUR', 2, 'Total') gave ${JSON.stringify(formatPrice(1250, 'EUR', 2, 'Total'))}`);
});

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

Any number of rounds

total should add the points of any number of rounds, passed one by one: total(a, b), total(), or total(...list). describe has no parameter type, so tsc reports error TS7006. Make rounds a rest parameter, and change describe to destructure player and points from a Round in its parameter list, with the annotation after the pattern and the return type string.

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

    A rest parameter has three dots before its name and an array type: ...rounds: Round[].

  2. Hint 2

    The body of total can stay as it is: rounds is still an array, empty when no rounds are passed.

  3. Hint 3

    export function describe({ player, points }: Round): string { return player + ": " + points; }

Show a solution

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

export type Round = { player: string; points: number };

export function total(...rounds: Round[]): number {
  let sum = 0;
  for (const round of rounds) sum += round.points;
  return sum;
}

export function describe({ player, points }: Round): string {
  return player + ": " + points;
}
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 type Round = { player: string; points: number };

export function total(rounds: Round[]): number {
  let sum = 0;
  for (const round of rounds) sum += round.points;
  return sum;
}

export function describe(round) {
  return round.player + ": " + round.points;
}

main.test.ts

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

const a = {player: 'Ada', points: 7};
const b = {player: 'Ada', points: 5};

test('total adds rounds passed one by one', () => {
  assert.equal(total(a, b), 12, 'total(a, b) should be 12');
});

test('total() with no rounds is 0', () => {
  assert.equal(total(), 0, 'total() should be 0');
});

test('an array can be spread into total', () => {
  const list = [a, b, {player: 'Grace', points: 9}];
  assert.equal(total(...list), 21, 'total(...list) should be 21');
});

test('describe names the player and the points', () => {
  assert.equal(describe(a), 'Ada: 7', `describe(a) gave ${JSON.stringify(describe(a))}`);
});

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

An optional parameter before a required one

function label(prefix?: string, name: string): string {
  return (prefix ?? "") + name;
}

console.log(label(undefined, "Ada"));

What tsc or Node.js prints

main.ts(1,33): error TS1016: A required parameter cannot follow an optional parameter.

Why, and the fix

Arguments are matched by position, so a caller cannot leave out the first and pass the second. Put the required parameters first: label(name: string, prefix?: string). If prefix must stay first, give it a default instead (prefix = ""), and callers pass undefined to use it.

Types written inside a destructuring pattern

function area({ width: number, height: number }) {
  return width * height;
}

console.log(area({ width: 3, height: 4 }));

What tsc or Node.js prints

main.ts(1,24): error TS2300: Duplicate identifier 'number'.

Why, and the fix

In a destructuring pattern, width: number means "take width and call the variable number", so there are two variables named number and none named width. The annotation goes after the whole pattern: function area({ width, height }: { width: number; height: number }).

An optional index in a callback type

function myForEach(arr: number[], callback: (arg: number, index?: number) => void) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i], i);
  }
}

myForEach([1, 2, 3], (a, i) => {
  console.log(i.toFixed());
});

What tsc or Node.js prints

main.ts(8,15): error TS18048: 'i' is possibly 'undefined'.

Why, and the fix

index?: number says myForEach might call the callback without an index, so every callback must cope with undefined, though the loop always passes one. Follow the Handbook's rule and write index: number. Callbacks that do not need it, such as (a) => console.log(a), still fit.

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

Optional parameters and defaults

A ? after a parameter name makes it optional: with function greet(name?: string), greet() is fine, and so is greet(undefined). Inside, name is string | undefined, because a missing argument is undefined, so name.toUpperCase() is error TS18048: 'name' is possibly 'undefined'. Check it first, or write name ?? "guest". A default does both jobs at once: in function repeat(text: string, times = 2), times needs no annotation, is a number in the body, and callers see times?: number. The default replaces only undefined: repeat("ab", 0) repeats zero times. Optional parameters come after the required ones, or tsc reports error TS1016. Extra arguments are still errors: error TS2554: Expected 1-2 arguments, but got 3.

Rest parameters, and spreading arguments

A rest parameter collects the remaining arguments into an array: function multiply(n: number, ...m: number[]). multiply(10, 1, 2) gives m = [1, 2], and multiply(10) an empty array. It must come last (error TS1014) and have an array or tuple type: ...m: number is error TS2370. Each argument is checked against the element type. The other way round, ... in a call spreads an array: sum(...prices) passes each price to the rest parameter. Spreading into fixed parameters, such as Math.atan2(y, x), needs a known length. const args = [8, 5] is number[], any length, so Math.atan2(...args) is error TS2556. [8, 5] as const is a tuple of exactly two numbers, and fits.

Destructured parameters, and optional parameters in callbacks

A destructured parameter gets its annotation after the whole pattern: function sum({ a, b, c }: { a: number; b: number; c: number }), or with a named type, ({ a, b, c }: ABC). Inside the pattern, a colon renames: { width: number, height: number } creates two variables called number, which tsc reports as error TS2300: Duplicate identifier 'number'. For callbacks, the Handbook gives a rule: never write an optional parameter in a callback type unless you will call the function without that argument. With callback: (arg: number, index?: number) => void, a caller's (a, i) => i.toFixed() is error TS18048: 'i' is possibly 'undefined'. Write index: number; callbacks that ignore it still fit (B4.1).

Sources

Last reviewed October 4, 2026