Everyday TypeScript: Function Parameters
Welcome to the Function Parameters lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
Functions declare what parameters they take, which in turn tell us what arguments, or inputs, we can pass to them. JavaScript's function parameters are quite flexible:
- We can specify default parameter values.
- When calling a function, we can pass fewer arguments than the function expects, effectively treating them as "optional parameters".
- We can pass different numbers of arguments when calling a function from different places.
TypeScript adds static type checking for all of these features.
An earlier lesson showed optional properties in objects, like
phoneNumber?: string. We can use the same syntax to declare optional function parameters. When we call the function but omit an optional parameter, that parameter gets the valueundefinedinstead.>
function add(x: number, y?: number) {return x + (y ?? 1);}[add(3, 4), add(3)];Result:
[7, 4]
Optional parameters must be declared after required parameters. If we put an optional parameter before a required one, we get a very specific type error message.
>
function add(x?: number, y: number) {return (x ?? 1) + y;}[add(3, 4), add(3)];Result:
type error: A required parameter cannot follow an optional parameter.
The examples above use optional parameters to implement a default value: if we provide only one argument,
addadds 1 to it. That works, but it complicates the types. The type of our optionalyargument becomesnumber | undefined, since we might calladdwithout providing ay.JavaScript and TypeScript also support default values directly. We can add a default value by putting
= someValuedirectly inside the function definition. If we call the function without specifying the parameter, it gets the default value. (Remember, if we don't specify an optional parameter,param?, it getsundefined.)>
function add(x: number, y: number = 1) {return x + y;}[add(10, 20), add(30)];Result:
[30, 31]
When possible, we recommend using default values instead of optional parameters. The types are simpler, avoiding the union with
undefined. Default values also save us from writing expressions likex ?? 1in the function body.The usual type rules still apply. If we have a typed parameter
y: number, we can't give it a default value ofundefined, likey: number = undefined. That's a type error becauseundefinedisn't allowed by thenumbertype.>
function add(x: number, y: number = undefined) {return x + (y ?? 1);}[add(3, 4), add(3)];Result:
type error: Type 'undefined' is not assignable to type 'number'.
If we really want to do that, we have to explicitly give the parameter a union type, like
number | undefined.>
function add(x: number, y: number | undefined = undefined) {return x + (y ?? 1);}[add(3, 4), add(3)];Result:
[7, 4]
JavaScript and TypeScript also support "rest parameters", which allow us to pass as many arguments as we want to our function. We declare them by putting
...someParameterNamein the function definition.In JavaScript, rest parameters are collected into an array. Because JavaScript is dynamically typed, that array can be mixed: it can contain values of any type. In TypeScript, we need to specify the types statically.
Here's a simple case where the function can only take number arguments.
>
function add(...numbers: number[]) {let sum = 0;for (const n of numbers) {sum += n;}return sum;}[add(), add(1, 2), add(100, 200, 300)];Result:
[0, 3, 600]
>
function add(...numbers: number[]) {let sum = 0;for (const n of numbers) {sum += n;}return sum;}add(1, 2, '3');Result:
type error: Argument of type 'string' is not assignable to parameter of type 'number'.
Since rest parameters are collected into an array, the type must always be an array. Anything else is a type error, with a very specific error message.
>
function add(...numbers: number) {let sum = 0;for (const n of numbers) {sum += n;}return sum;}Result:
type error: A rest parameter must be of an array type.
Sometimes we want a rest parameter that allows multiple types of rest arguments, like
f(1, '2'). In that case we can use an array of a union type, likeArray<number | string>.Here's a code problem:
The
maximumfunction below has an incorrect type. Modify the rest parameter's type to allowundefinedin the rest arguments. You won't need to change the function body; itsifconditional already ignoresundefinedvalues.function maximum(...numbers: Array<number | undefined>) {let max = -Infinity;for (const n of numbers) {if (n !== undefined && n > max) {max = n;}}return max;}[maximum(undefined, 1), maximum(5, 100, undefined)];- Goal:
[1, 100]
- Yours:
[1, 100]
We've seen three function parameter features in this lesson: optional parameters, default parameters, and rest parameters. So far, we've only used them directly inside of function definitions. But optional and rest parameters are also allowed inside of function types.
>
type AddFunction = (x: number, y?: number) => number;const add: AddFunction = (x, y) => {return x + (y ?? 1);};[add(3, 4), add(3)];Result:
[7, 4]
Here's a code problem:
The function body below is correct, but the
AddFunctiontype is missing a piece. Add in the missing rest parameter type so the function type checks.type AddFunction = (...numbers: number[]) => number;const add: AddFunction = (...numbers) => {let sum = 0;for (const n of numbers) {sum += n;}return sum;};[add(), add(1, 2), add(100, 200, 300)];- Goal:
[0, 3, 600]
- Yours:
[0, 3, 600]
Unlike optional and rest parameters, default parameter values aren't allowed inside function types. That's a type error.
>
type AddFunction = (x: number, y: number = 1) => number;Result:
type error: A parameter initializer is only allowed in a function or constructor implementation.
This may seem strange at first: why do function types allow optional and rest parameters, but disallow default parameter values? The answer comes from the difference between types and runtime values. Here are two different ways to think about it:
First, default values like the
1iny: number = 1aren't types; they're concrete values that exist at runtime. When we call the function, that1is going to end up in a local variable. But types can never contain concrete runtime values like the actual object{name: 'Amir'}or the actual number1. Types can only contain other types, like{name: string}ornumber, or even the literal type1.(This is a bit confusing in the case of
1, since the value1and the literal type1are written in the same way. But one only exists at runtime and the other only exists in the type system.)Second, the TypeScript compiler ultimately outputs JavaScript code. But, since TypeScript types are simply thrown away after type checking ends, they never show up in the compiled JavaScript. If types, including function types, never exist at runtime, then it doesn't make sense for them to contain concrete runtime values like the actual number
1.Because all types are thrown away, the default value would be thrown away too. There would be no way for that default value to make it into the compiled JavaScript code.
Adding static types on top of JavaScript's parameter syntax allows us to take full advantage of the TypeScript type system, but retain the flexibility of JavaScript's function parameters.