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 includesundefined. - Default parameter
- A parameter with
= value. If the argument is missing orundefined, the default is used. - Rest parameter
- A last parameter written
...namethat gathers all remaining arguments into an array. - Spread
- Writing
...arrayat 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.
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.
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.
function sum(...nums: number[]) {
return nums.reduce((a, b) => a + b, 0);
}
sum(); // 0
sum(1, 2, 3); // 6
sum(...[4, 5]); // 904A 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.
type Options = { width: number; height?: number };
function area({ width, height = 10 }: Options) {
return width * height;
}Try it yourself
- 1
Predict the hover
In the second example, change
tax = amount * 0.18totax = 0. Predict the hover fortaxbefore you look: stillnumber. Now change it totax = "none". The body has no error (a number plus a string is legal JavaScript, it joins them), but the hover now readstax?: stringand the callspay(100, "USD", 0)andpay(100, undefined, 5)becomeTS2345errors. What did the default decide? - 2
Rest after rest
In the third example add a second rest parameter after
...numsinsum. 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
Skip with undefined
Call
pay(100, null, 5)in the second example. Predict: isnullthe same as a missing argument? The default only triggers forundefined, and TypeScript rejectsnullbecause the type isstring.
Code & diagrams
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, AshaNobody 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 INRThe 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' ]
15The 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
x1Break 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;.
Break #2
Put an optional parameter before a required one
Write function greet(greeting?: string, name: string).
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);.
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
undefinedare not the same to the checker:f(a?: number)can be called asf(), whilef(a: number | undefined)must be called asf(undefined)at least. WithexactOptionalPropertyTypesthe 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_0andparts_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 plainnumber[]into fixed positions (pair(...more)) isTS2556: 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 oras constarray 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
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 isstring | undefined(the type, orundefinedwhen nothing was passed), so TypeScript makes you handle the missing case before you use it. Usinggreeting.toUpperCase()directly isTS18048: 'greeting' is possibly 'undefined'. - 2
Optional parameters must come after all required ones.
function f(a?: number, b: number)isTS1016: A required parameter cannot follow an optional parameter. The reason is simple: when a caller writesf(1), the checker needs to know which parameter the1is for. - 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 (stringhere). From the outside the parameter is optional (currency?: string); inside the function it is neverundefined, because the default fills the gap. A default can even use earlier parameters:tax = amount * 0.18. - 4
Only
undefinedtriggers a default;null,0and""do not. Sopay(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 passundefinedexplicitly to get the default, and its type becomesnumber | undefinedfrom the outside. - 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)andsum(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[]orArray<number>) or a tuple (an array with a fixed length and a type for each position, covered in Topic 5.2). - 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")isTS2554: Expected 2 arguments, but got 1, and with the optional element,log("info")forlog(level: string, ...parts: [string, number?])isExpected 2-3 arguments, but got 1. Tuple rest parameters are also how TypeScript describes the arguments of any function (tuples: Topic 5.2, theParameters<T>utility that reads them: Topic 10.2). - 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 renamewidthtonumber. Put the type on the pattern, or name it with atypeorinterfacefor readability. A default inside the pattern (height = 10) works like any default, and the type annotation can markheightas optional.
Explain it without notes
What is the difference between an optional parameter and a parameter with a default value?
Where can optional and rest parameters appear in a parameter list, and why?
How do you type a function that takes any number of numbers? How do you call it with an array?
How do you put a type on a destructured parameter, and what is the common mistake?
Practice
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, "+").
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.
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
undefineda 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 | undefinedinside.I can write a default and say what type it infers.
I can explain why only
undefinedtriggers 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.