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:
2
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:
3
>
type TakesNumberReturnsNumber = (x: number) => number;const add1: (x: number) => number = x => x + 1;const arrayOfFunctions: TakesNumberReturnsNumber[] = [add1];arrayOfFunctions[0](3);Result:
4
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:
5
Why did we put (parentheses) around the function type in
((x: number) => number)[]? Because without them, the type ofarrayOfFunctionswould 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
callFunctionfunction 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:
3
>
function callFunction(f: (x: number) => number) {return f(10);}callFunction(x => x + 1);Result:
11
The
callFunctionfunction above passed a hard-coded argument tof. Instead of hard-coding the argument, we can pass it tocallFunction, which then passes it along tof.>
function callFunction(f: (x: number) => number, x: number) {return f(x);}callFunction(x => x + 1,1);Result:
2
>
function callFunction(f: (x: number) => number, x: number) {return f(x);}callFunction(x => x + 1,2);Result:
3
Everything in TypeScript has to have a type, so what's the return type of our
callFunctionfunction? The compiler reasons about it like this:callFunctionreturnsf(x), socallFunction's return type must matchf's return type.f's return type isnumber, as shown in its overall type(x: number) => number.- So
callFunction's return type is alsonumber.
We can take this as far as we want. As a demonstration, here's a
buildDoublerfunction. It takes a functionfas an argument, which itself returns a number.buildDoublerthen returns a new function that returns2 * 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:
6
The
buildDoublerfunction 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!