Course outline · 0% complete

0/26 lessons0%

Course overview →

Typing Callbacks

lesson 5-2 · ~11 min · 16/26

Functions as values need types too

In Advanced JavaScript you passed functions to map, filter, and event handlers. A function type describes such a value: its parameters and its return type, written with an arrow.

function applyTwice(value: number, fn: (n: number) => number): number {
  return fn(fn(value));
}

Read (n: number) => number as "a function that takes a number and returns a number". Now the compiler checks both sides of the deal:

  • Passing (n) => n + 1 is fine
  • Passing (s: string) => s is an error, wrong parameter type
  • Inside the body, calling fn(true) is an error too

The callback bugs you debugged by hand in JavaScript become one-line compile errors.

Feeding a value through a checked callback

function applyTwice(value: number, fn: (n: number) => number): number {
  return fn(fn(value));
}

console.log(applyTwice(5, (n) => n + 1));
console.log(applyTwice(3, (n) => n * n));

Output

7
81

The first call computes fn(fn(5)), which is fn(6), which is 7. The second squares twice, so 3 becomes 9 and then 81.

Calling applyTwice(5, (s: string) => s) fails to compile, and where the failure lands is the point. The mismatch is reported at the call site, naming the argument that does not fit, rather than exploding somewhere inside applyTwice at runtime with a message about a method that does not exist. Function types move callback bugs from the debugger to the editor.

5fn(5) = 6fn(6) = 77applyTwice(5, fn) where fn: (n: number) => numberthe function type guarantees each step takes and returns a number
applyTwice feeds the value through the callback twice. The function type (n: number) => number checks every hop.

Contextual typing: the callback knows its own types

Notice (n) => n + 1 needed no annotation on n. TypeScript already knows the expected callback type from applyTwice, so it types n as number from context. This is contextual typing, and it is why array methods feel so smooth:

const prices: number[] = [3, 10, 6];
const labels = prices.map((p) => "$" + p);

p is automatically number because prices is number[], and labels is inferred as string[] because the callback returns strings. The same map, filter, and reduce you know, now with every step checked.

Array methods with no annotations at all

Neither p nor sum below carries a type annotation. Both are typed entirely from context.

const prices: number[] = [3, 10, 6];

const labels = prices.map((p) => "$" + p);
const total = prices.reduce((sum, p) => sum + p, 0);

console.log(labels.join(" "));
console.log("total: $" + total);

Output

$3 $10 $6
total: $19

Where each type comes from

  • p is a number because the array is number[]. Change "$" + p to p.toUpperCase() and the compiler rejects it, since string methods do not exist on numbers.
  • labels is inferred as string[] because the callback returns a string, so labels.join(" ") is available without any declaration.
  • In reduce, sum takes its type from the initial value 0, which is why the accumulator is a number and sum + p is arithmetic rather than concatenation.

countMatching: a named function type as a parameter

WordRule names the shape of a test on a word, and countMatching counts how many items in an array pass whichever rule it is handed.

type WordRule = (word: string) => boolean;

function countMatching(items: string[], rule: WordRule): number {
  let count = 0;
  for (const item of items) {
    if (rule(item)) {
      count++;
    }
  }
  return count;
}

const animals = ["cat", "horse", "dog", "elephant"];
console.log(countMatching(animals, (w) => w.length <= 3));
console.log(countMatching(animals, (w) => w.charAt(0) === "h"));

Output

2
1

Reading the pieces

  • A type alias can name a function type, exactly as it named object shapes in lesson 3-3. Writing rule: WordRule is shorter and more readable than repeating (word: string) => boolean at every use.
  • The loop body is if (rule(item)) { count++; }. Calling the parameter looks no different from calling any other function.
  • The first rule matches cat and dog, both of length 3. The second matches only horse, the one word beginning with h.
  • Both callbacks are written without annotating w, since contextual typing pulls string from WordRule.