Execute Program

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 value undefined instead.

  • >
    function add(x: number, y?: number) {
    return x + (y ?? 1);
    }
    [add(3, 4), add(3)];
    Result:
    [7, 4]Pass Icon
  • 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.Pass Icon
  • The examples above use optional parameters to implement a default value: if we provide only one argument, add adds 1 to it. That works, but it complicates the types. The type of our optional y argument becomes number | undefined, since we might call add without providing a y.

  • JavaScript and TypeScript also support default values directly. We can add a default value by putting = someValue directly 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 gets undefined.)

  • >
    function add(x: number, y: number = 1) {
    return x + y;
    }
    [add(10, 20), add(30)];
    Result:
    [30, 31]Pass Icon
  • 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 like x ?? 1 in 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 of undefined, like y: number = undefined. That's a type error because undefined isn't allowed by the number type.

  • >
    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'.Pass Icon
  • 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]Pass Icon
  • 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 ...someParameterName in 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]Pass Icon
  • >
    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'.Pass Icon
  • 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.Pass Icon
  • 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, like Array<number | string>.

  • Here's a code problem:

    The maximum function below has an incorrect type. Modify the rest parameter's type to allow undefined in the rest arguments. You won't need to change the function body; its if conditional already ignores undefined values.

    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]Pass Icon
  • 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]Pass Icon
  • Here's a code problem:

    The function body below is correct, but the AddFunction type 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]Pass Icon
  • 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.Pass Icon
  • 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 1 in y: number = 1 aren't types; they're concrete values that exist at runtime. When we call the function, that 1 is 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 number 1. Types can only contain other types, like {name: string} or number, or even the literal type 1.

  • (This is a bit confusing in the case of 1, since the value 1 and the literal type 1 are 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.