Execute Program

Everyday TypeScript: Functions as Arguments

Welcome to the Functions as Arguments lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • JavaScript functions can be assigned to variables, stored in arrays and objects, and passed as function arguments. We say that JavaScript has "first-class functions", because functions are a first-class part of the language with no special restrictions. Not all languages have first-class functions!

  • TypeScript can do everything that JavaScript can do, so TypeScript also has first-class functions. We've already put functions into statically typed variables. Now we'll take that further: we'll put functions in arrays, and we'll pass functions as arguments to other functions.

  • First, here's our function. We can put it in a variable and call the variable directly.

  • >
    type TakesNumberReturnsNumber = (x: number) => number;
    const add1: (x: number) => number = x => x + 1;
    const aFunction: TakesNumberReturnsNumber = add1;
    aFunction(1);
    Result:
    2Pass Icon
  • We can put the function in an array, then take it out and call it. First, we'll do that using an intermediate variable; then, we'll do it in a single step.

  • >
    type TakesNumberReturnsNumber = (x: number) => number;
    const add1: (x: number) => number = x => x + 1;
    const arrayOfFunctions: TakesNumberReturnsNumber[] = [add1];
    const aFunction: TakesNumberReturnsNumber = arrayOfFunctions[0];
    aFunction(2);
    Result:
    3Pass Icon
  • >
    type TakesNumberReturnsNumber = (x: number) => number;
    const add1: (x: number) => number = x => x + 1;
    const arrayOfFunctions: TakesNumberReturnsNumber[] = [add1];
    arrayOfFunctions[0](3);
    Result:
    4Pass Icon
  • We can choose between naming our function type, like TakesNumberReturnsNumber, or writing the whole function type out explicitly.

  • >
    type TakesNumberReturnsNumber = (x: number) => number;
    const add1: (x: number) => number = x => x + 1;
    const arrayOfFunctions: ((x: number) => number)[] = [add1];
    arrayOfFunctions[0](4);
    Result:
    5Pass Icon
  • Why did we put (parentheses) around the function type in ((x: number) => number)[]? Because without them, the type of arrayOfFunctions would be (x: number) => number[]. That's a very different type: it describes a function that takes one number and returns an array of numbers (number[]). It fails to type check:

  • >
    type TakesNumberReturnsNumber = (x: number) => number;
    const add1: (x: number) => number = x => x + 1;
    const arrayOfFunctions: (x: number) => number[] = [add1];
    arrayOfFunctions[0](4);
    Result:
  • We can pass functions as arguments to other functions. Passing functions as arguments always feels like a big jump to a human. But from the type checker's perspective, it's just like any other type. Our callFunction function below takes another function as an argument. If the types match, the compiler will be happy.

  • >
    function callFunction(f: () => number) {
    return f();
    }
    callFunction(
    () => 3
    );
    Result:
    3Pass Icon
  • >
    function callFunction(f: (x: number) => number) {
    return f(10);
    }
    callFunction(
    x => x + 1
    );
    Result:
    11Pass Icon
  • The callFunction function above passed a hard-coded argument to f. Instead of hard-coding the argument, we can pass it to callFunction, which then passes it along to f.

  • >
    function callFunction(f: (x: number) => number, x: number) {
    return f(x);
    }
    callFunction(
    x => x + 1,
    1
    );
    Result:
    2Pass Icon
  • >
    function callFunction(f: (x: number) => number, x: number) {
    return f(x);
    }
    callFunction(
    x => x + 1,
    2
    );
    Result:
    3Pass Icon
  • Everything in TypeScript has to have a type, so what's the return type of our callFunction function? The compiler reasons about it like this:

    • callFunction returns f(x), so callFunction's return type must match f's return type.
    • f's return type is number, as shown in its overall type (x: number) => number.
    • So callFunction's return type is also number.
  • We can take this as far as we want. As a demonstration, here's a buildDoubler function. It takes a function f as an argument, which itself returns a number. buildDoubler then returns a new function that returns 2 * f().

  • >
    function buildDoubler(
    f: () => number
    ): () => number {
    return () => {
    return 2 * f();
    };
    }
  • Now we'll use it to prove that it works:

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    function always3() {
    return 3;
    }

    const returns3Doubled = buildDoubler(always3);
    returns3Doubled();
    Result:
    6Pass Icon
  • The buildDoubler function is confusing for no good reason. It suggests a very important point about TypeScript, or type systems in general, or even programming in general: the compiler and computer don't care about how confusing something is. They only care about whether it follows the rules.

  • We can write functions that take argument types like () => (f: () => number) => () => (f: () => number) => number. That's not a problem for the compiler at all, but it's definitely a problem for humans. "The code has valid types" is a starting point, but it's important to make the code readable, not just valid!