Command Palette

Search for a command to run...

Hectal
PHASE 2Beginner ~27 min· topic 2 of 7

Topic 2.2

Optional, Default and Rest Parameters

In one line

Some arguments can be left out (optional), some have a ready-made value (default), and some functions take any number of them (rest). Each form changes the type TypeScript gives the parameter.

Think of it like this

Ordering a coffee. You must say the size. Milk is optional: if you say nothing, the barista asks or leaves it out. Sugar has a default: say nothing and you get two spoons. And toppings are "any number": you can add none, one or ten. Parameters work the same way: required, optional, with a default, or "as many as you like".

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Optional parameter
A parameter marked with ? that the caller may leave out. Its type inside the function includes undefined.
Default parameter
A parameter with = value. If the argument is missing or undefined, the default is used.
Rest parameter
A last parameter written ...name that gathers all remaining arguments into an array.
Spread
Writing ...array at a call to unpack the array into separate arguments.
Tuple
An array with a fixed length and a known type at each position, like [string, number].
Destructuring
Pulling values out of an object or array straight into variables, including in a parameter list.
Arity
How many parameters a function takes.
undefined
The value JavaScript gives to anything that was not provided. An omitted optional argument is undefined.

Step by step

01An optional parameter is T | undefined

Add ? after the name. The caller can now write greet("Asha") or greet("Asha", "Namaste").

Inside, greeting could be missing, so its type is string | undefined. The ?? operator (use the right side if the left is null or undefined) gives a safe fallback.

greet.tswhole filets
function greet(name: string, greeting?: string) {
  return (greeting ?? "Hello") + ", " + name;
}

02A default is optional outside, a plain type inside

currency = "INR" makes the type string without writing it. The caller sees currency?: string. Inside the body it is always a string.

Defaults are computed fresh on every call, and can read earlier parameters. Pass undefined to "skip" one and use the default.

pay.tswhole filets
function pay(amount: number, currency = "INR", tax = amount * 0.18) {
  return `${amount + tax} ${currency}`;
}
pay(100);                 // "118 INR"
pay(100, undefined, 5);   // "105 INR"

03Rest gathers, spread unpacks

...nums: number[] turns every extra argument into one array. With no extra arguments the array is empty, not undefined.

At the call, sum(...more) does the reverse: it unpacks more into separate arguments.

sum.tswhole filets
function sum(...nums: number[]) {
  return nums.reduce((a, b) => a + b, 0);
}
sum();            // 0
sum(1, 2, 3);     // 6
sum(...[4, 5]);   // 9

04A tuple as a rest parameter

When the extra arguments have different types in a fixed order, type the rest parameter as a tuple. [string, number?] means a string first, then an optional number.

The caller sees ordinary parameters, and the checker enforces the count and each type.

05Destructure with the type on the pattern

The annotation comes after the closing brace. Put the shape in a named type so the signature stays readable.

Defaults inside the pattern work as usual: height = 10 fills in when height is missing.

area.tswhole filets
type Options = { width: number; height?: number };

function area({ width, height = 10 }: Options) {
  return width * height;
}
Destructure with the type on the patterndiagram
Rendering diagram…

Try it yourself

  1. 1

    Predict the hover

    In the second example, change tax = amount * 0.18 to tax = 0. Predict the hover for tax before you look: still number. Now change it to tax = "none". The body has no error (a number plus a string is legal JavaScript, it joins them), but the hover now reads tax?: string and the calls pay(100, "USD", 0) and pay(100, undefined, 5) become TS2345 errors. What did the default decide?

  2. 2

    Rest after rest

    In the third example add a second rest parameter after ...nums in sum. Predict which rule it breaks, then read the message: TS1014: A rest parameter must be last in a parameter list. (A function can have only one rest parameter, and it has to be the last.)

  3. 3

    Skip with undefined

    Call pay(100, null, 5) in the second example. Predict: is null the same as a missing argument? The default only triggers for undefined, and TypeScript rejects null because the type is string.

Code & diagrams

An optional parameter and the ?? fallback

The hover shows greeting?: string. Inside the body, greeting is string | undefined, which is why ?? is needed. Replace the body with greeting.toUpperCase() and read the error.

Console

(nothing was printed)

Expected console output

Hello, Asha
Namaste, Asha
Default parameters: inferred types, skipping with undefined

Nobody wrote string or number for currency and tax: the defaults gave the types. The last line passes a number where the default's type string is expected.

Console

(nothing was printed)

Expected console output

118 INR
100 USD
105 INR
Rest parameters with arrays and tuples

The tuple rest parameter shows up in the hover as separate parameters. The last line has the right type for the first slot but is missing the second.

Console

(nothing was printed)

Expected console output

0 6
info [ 'disk', 90 ]
info [ 'disk' ]
15
Typed parameter destructuring

The type goes after the whole pattern. The last function has a default for the whole object, but the default is missing a required property, so tsc rejects the default itself.

Console

(nothing was printed)

Expected console output

50 10
x1

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use an optional parameter without checking

In the first example replace the body with return greeting.toUpperCase() + ", " + name;.

terminal
$ Types panel
── what you'll see ──
index.ts(3,10): error TS18048: 'greeting' is possibly 'undefined'.

Break #2

Put an optional parameter before a required one

Write function greet(greeting?: string, name: string).

terminal
$ Types panel
── what you'll see ──
index.ts(1,35): error TS1016: A required parameter cannot follow an optional parameter.

Break #3

Wrong type in the rest list

At the bottom of the third example add function total(prefix: string, ...nums: number[]) {} and, on the next line, the call total("n=", 1, "2");. Afterwards also try total(1, 2);.

terminal
$ Types panel
── what you'll see ──
index.ts(16,16): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

Myth vs fact

Myth

A default parameter and an optional parameter are the same.

Fact

Both let the caller omit the argument. An optional parameter is T | undefined inside the function; a default is T inside, because the default fills the gap. A default also fires when the caller passes undefined explicitly.

Myth

Defaults replace any falsy value, like || does.

Fact

Defaults only replace undefined. 0, "", false and null are passed through unchanged, which is usually what you want.

Myth

function f({ name: string }) types name as a string.

Fact

That destructuring renames the property name to a local variable called string. The type annotation goes after the pattern: function f({ name }: { name: string }).

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Optional parameters and parameters whose type includes undefined are not the same to the checker: f(a?: number) can be called as f(), while f(a: number | undefined) must be called as f(undefined) at least. With exactOptionalPropertyTypes the difference matters for object properties too (Topic 1.5), but never for parameters.

  • ▸

    A rest parameter of a tuple type is expanded by the checker into a list of ordinary parameters (that is why the hover shows parts_0 and parts_1). The names come from the tuple's labels if it has them ([first: string, second?: number], TypeScript 4.0), which is a good way to keep error messages readable.

  • ▸

    Spreading into a call (sum(...more)) requires the array type to match the rest element type, but spreading a plain number[] into fixed positions (pair(...more)) is TS2556: A spread argument must either have a tuple type or be passed to a rest parameter: the checker can't know the array has two items. A tuple or as const array can be spread into fixed positions.

  • ▸

    Default values are evaluated at call time in their own scope, which sees the earlier parameters but not variables from the function body. A default that throws or does expensive work runs on every call that omits the argument.

Remember this

  1. 1

    The problem this solves: Some inputs may be left out, have a sensible default, or come in any number. Without a way to say so, callers guess, and a missing value silently becomes undefined. An optional parameter has a ? after its name: greeting?: string. The caller may leave it out. Inside the function its type is string | undefined (the type, or undefined when nothing was passed), so TypeScript makes you handle the missing case before you use it. Using greeting.toUpperCase() directly is TS18048: 'greeting' is possibly 'undefined'.

  2. 2

    Optional parameters must come after all required ones. function f(a?: number, b: number) is TS1016: A required parameter cannot follow an optional parameter. The reason is simple: when a caller writes f(1), the checker needs to know which parameter the 1 is for.

  3. 3

    A default parameter gives the parameter a value to use when the argument is missing or is undefined: currency = "INR". You don't need to write a type: TypeScript infers it from the default (string here). From the outside the parameter is optional (currency?: string); inside the function it is never undefined, because the default fills the gap. A default can even use earlier parameters: tax = amount * 0.18.

  4. 4

    Only undefined triggers a default; null, 0 and "" do not. So pay(100, undefined, 5) skips the middle argument and still uses "INR". This is also how you "skip" a middle parameter when you want a later one. Unlike optional parameters, a parameter with a default may come before a required one: the caller must then pass undefined explicitly to get the default, and its type becomes number | undefined from the outside.

  5. 5

    A rest parameter collects the extra arguments into an array. It is written ...nums: number[], comes last, and a function can have only one. sum(), sum(1) and sum(1, 2, 3) all work. The opposite syntax is spread at the call: sum(...more) unpacks an array into separate arguments. Rest must be typed as an array (number[] or Array<number>) or a tuple (an array with a fixed length and a type for each position, covered in Topic 5.2).

  6. 6

    A rest parameter typed with a tuple lets you describe a fixed argument list in one place: ...parts: [string, number?] means "a string, and optionally a number". TypeScript shows it to the caller as two normal parameters (parts_0: string, parts_1?: number | undefined), and a call with the wrong count fails like an ordinary one: for ...args: [string, number], f("a") is TS2554: Expected 2 arguments, but got 1, and with the optional element, log("info") for log(level: string, ...parts: [string, number?]) is Expected 2-3 arguments, but got 1. Tuple rest parameters are also how TypeScript describes the arguments of any function (tuples: Topic 5.2, the Parameters<T> utility that reads them: Topic 10.2).

  7. 7

    You can destructure a parameter: pull an object or array apart right in the parameter list. The type goes after the whole pattern, not inside it: function area({ width, height = 10 }: Options). Writing { width: number } there would not mean "width is a number"; it would rename width to number. Put the type on the pattern, or name it with a type or interface for readability. A default inside the pattern (height = 10) works like any default, and the type annotation can mark height as optional.

Explain it without notes

01

What is the difference between an optional parameter and a parameter with a default value?

02

Where can optional and rest parameters appear in a parameter list, and why?

03

How do you type a function that takes any number of numbers? How do you call it with an array?

04

How do you put a type on a destructured parameter, and what is the common mistake?

Practice

01

Write function repeat(text: string, times = 2, separator?: string) that returns the text repeated times times, joined by the separator (or nothing if there is none). Log repeat("ab"), repeat("ab", 3, "-") and repeat("ab", undefined, "+").

02

Write function maxOf(first: number, ...rest: number[]) that returns the largest number. Why is first separate from rest? Call it with an array of three numbers using spread.

03

Write function label({ name, age = 18 }: { name: string; age?: number }) that returns "Asha (18)". Then call label({ name: "Asha", age: "x" }) and note the error.

Trade-offs

  • ↔

    Many optional parameters make a call hard to read (f(1, undefined, undefined, true)). Past three or so, take one object parameter with named, optional properties (Topic 1.5) instead.

  • ↔

    A default hides the "missing" case from the function body, which keeps the code simple, but it also makes undefined a valid argument that callers can pass by accident. Make sure that is the behaviour you want.

Done when you can

  • I can mark a parameter optional and handle string | undefined inside.

  • I can write a default and say what type it infers.

  • I can explain why only undefined triggers a default.

  • I can type a rest parameter with an array and with a tuple, and call it with spread.

  • I can put a type on a destructured parameter without renaming anything.