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 + 1is fine - Passing
(s: string) => sis 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.
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
pis anumberbecause the array isnumber[]. Change"$" + ptop.toUpperCase()and the compiler rejects it, since string methods do not exist on numbers.labelsis inferred asstring[]because the callback returns a string, solabels.join(" ")is available without any declaration.- In
reduce,sumtakes its type from the initial value0, which is why the accumulator is anumberandsum + pis 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: WordRuleis shorter and more readable than repeating(word: string) => booleanat 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
catanddog, both of length 3. The second matches onlyhorse, the one word beginning withh. - Both callbacks are written without annotating
w, since contextual typing pullsstringfromWordRule.