Before you begin
Why this matters
Many functions you use take a function from you: a sort order, a rule for each item, code to run later. If you can read the type of a function and tell a function from its result, you can predict what such code does and spot a missing or extra pair of parentheses.
What this helps you reason about
- Follow a function you passed in: the function that received it calls it, once per item.
- Read a type such as (points: number) => number, and a function that returns one.
- Spot a function used where its result was needed, such as text that prints a function’s code.
A function can take another function as an argument, and it can return a new function. This lesson shows how a function uses behavior it receives, how a function builds behavior and returns it, how TypeScript writes the type of a function, and the mistake of calling a function where the function itself was needed.
Already know this? The short version
- applyToEach(scores, addBonus) passes addBonus without calling it, and applyToEach calls it once per element. A function passed this way is a callback; a function that takes or returns a function is a higher-order function.
- A factory such as makeScorer(3) runs once and returns a new function; that function runs each time it is called, after the factory’s frame is gone.
- A function type lists the parameters and the result: (points: number) => number. A function that returns one is declared as makeScorer(difficulty: number): (points: number) => number.
- f passes the function and f(8) passes its result. TypeScript rejects a number where a function is expected, but "8 becomes " + rule compiles and prints the function’s code.
Words in this lesson (6)
- callback
- A function passed to another function, which calls it, for example once for each element of an array.
- higher-order function
- A function that takes a function as an argument, returns a function, or both.
- factory
- A function whose job is to make and return a new value, such as a new function built from its arguments.
- arrow function
- A function written as a value, without a name, such as () => "stall " + i. The part before => lists its parameters, and the part after it is the value it returns.
- closure
- A function together with the variables from the surrounding code that it uses. Those variables last as long as the function does.
- function type
- The type of a function value, written with its parameters and its result, such as (points: number) => number: a function that takes a number and returns a number.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Callbacks
How does a function accept behavior?
Function calls showed that a function is a value: written without parentheses, it is passed, not called. applyToEach below uses this. It goes through an array of scores and builds a new array, but it does not decide what to do with each score. Its caller passes that in, as the function rule, and applyToEach calls rule(points) for each score.
A function passed to another function, which calls it, is called a callback. A function that takes a function as an argument, or returns one, is called a higher-order function. applyToEach is a higher-order function. addBonus, which returns points + 2, is an ordinary function, and here it is the callback. With another callback, such as doublePoints, the same applyToEach doubles each score.
Sorting works the same way: years.sort(byYear) passes a compare function that decides which of two elements comes first. Ordering, sorting, and comparators follows that callback in detail.
function applyToEach(
scores: number[],
rule: (points: number) => number,
): number[] {
const results: number[] = [];
for (const points of scores) {
results.push(rule(points));
}
return results;
}
const results = applyToEach([6, 8, 7], addBonus);
console.log(results.join(" ")); // 8 10 9 A higher-order function receives behavior as a function, and decides when to call it.
Coming from Java: There is no interface to choose and no applyAsInt: any function whose parameters and result fit (points: number) => number can be passed, and rule(points) calls it directly.
Precisely: a callback receives what it is given
The higher-order function decides which arguments the callback gets. The array method map calls its callback with three: the element, its index, and the array. parseInt takes a second parameter, the base, so ["1", "2", "3"].map(parseInt) passes the index as the base and gives 1, NaN, and NaN. TypeScript accepts it, because the types fit.
Experiment
Follow a rule into the function that calls it
applyToEach receives the rule as a value and calls it as rule(points), once per score. Which function runs is decided by the caller, in the call to applyToEach. applyToEach never names addBonus or doublePoints.
Why it matters To change what a higher-order function does with each item, change the function you pass to it. The higher-order function stays the same.
Predict the printed numbers, and which frames are in the frame list while addBonus runs. Then change applyToEach([6, 8, 7], addBonus) so that it passes doublePoints instead, and predict again.
function applyToEach(
scores: number[],
rule: (points: number) => number,
): number[] {
const results: number[] = [];
for (const points of scores) {
results.push(rule(points));
}
return results;
}
function addBonus(points: number): number {
return points + 2;
}
function doublePoints(points: number): number {
return points * 2;
}
const results = applyToEach([6, 8, 7], addBonus);
console.log(results.join(" "));
What to look for
Step into applyToEach: its frame lists rule as function addBonus(points), the function that was passed. Each time line 7 runs rule(points), a frame named addBonus appears on top of applyToEach, which waits, on top of <top level>. addBonus’s frame lists points 6, then 8, then 7, and results grows to [8, 10, 9]. The run prints 8 10 9. After the edit, rule shows function doublePoints(points), the frames on top are named doublePoints, and the run prints 12 16 14. The code of applyToEach did not change. <top level> is the stepper’s name for the code outside every function. A variable whose line has not run yet shows as not initialized yet. Step sometimes stops on the same line two or three times; that is normal for this debugger.
Expected output
8 10 902 / Factories
How does a function return new behavior?
A function can also return a function. makeScorer below takes a difficulty and returns an arrow function that multiplies points by it. Its result type, (points: number) => number, says so. A function whose job is to make and return a new value, here a function, is often called a factory.
Calling makeScorer(3) runs makeScorer once. It computes no score: it returns a function, and hard holds it. That function runs later, each time hard(6) or hard(8) is called, when makeScorer’s frame is gone. It still has difficulty because it is a closure, as Closures and captured variables showed.
applyToEach([6, 8, 7], makeScorer(2)) passes the result of a call, and that is right here: the result is a function. makeScorer(3)(8) does both steps in one expression. The first call returns a function, and (8) calls that function.
function makeScorer(difficulty: number): (points: number) => number {
return (points) => points * difficulty;
}
const hard = makeScorer(3);
console.log(hard(6), hard(8)); // 18 24
const doubled = applyToEach([6, 8, 7], makeScorer(2));
console.log(doubled.join(" ")); // 12 16 14 A factory runs once and returns a function; the returned function runs at each call.
Precisely: a new function each time
Each call to makeScorer creates a new function object. makeScorer(3) === makeScorer(3) is false, although the two functions behave the same, because === compares objects by identity.
Experiment
Make a scorer, then call it
Calling makeScorer runs its body once: it makes a new function and returns it. The returned function runs only when it is called, after makeScorer has returned, and it still has difficulty because it captured it. In the edit, makeScorer(3)(8) calls makeScorer again and then calls the function it returns.
Why it matters A factory runs when it is called. The function it returns runs each time that function is called. To use the same rule many times, call the factory once and keep its result.
Predict the two numbers, and whether makeScorer’s frame is in the frame list while the scorer runs. Then change hard(8) to makeScorer(3)(8), and predict: how many times does makeScorer run now?
function makeScorer(difficulty: number): (points: number) => number {
return (points) => points * difficulty;
}
const hard = makeScorer(3);
console.log(hard(6), hard(8));
What to look for
At the first stops, makeScorer’s frame has difficulty 3. When it returns, its frame is gone, and hard shows arrow function (points). Each call, hard(6) and hard(8), gets a frame named arrow function (line 2), with points and difficulty 3, directly on top of <top level>: makeScorer is not in the list. The run prints 18 24. After the edit, makeScorer’s frame appears a second time, just before the second scorer frame, and the run still prints 18 24.
Expected output
18 2403 / Types
How do you write the type of a function?
A function type lists the parameter types and the result type. (points: number) => number is a function that takes a number and returns a number. The name points in the type is only a label: any function with one number parameter and a number result fits, whatever its parameter is called.
Function types can be nested. makeScorer’s own type is (difficulty: number) => (points: number) => number: it takes a number and returns a function. The keyword type gives a long type a short name, so type Rule = (points: number) => number lets the code write Rule instead.
TypeScript checks a function against the type where it is passed. A function with fewer parameters than the type also fits, because the extra arguments are ignored. A function that needs more parameters, or other types, does not.
type Rule = (points: number) => number;
const bonus: Rule = (points) => points + 2;
const factory: (difficulty: number) => Rule = makeScorer;
const keepTen: Rule = () => 10; // fewer parameters: fits A function type says what a function takes and what it returns; parameter names in it are only labels.
Precisely: checked before the program runs
TypeScript removes all types when it compiles to JavaScript. At run time, a function value carries no parameter or result types, and nothing checks a callback’s arguments. Only the compiler’s check, before the run, keeps the wrong function out.
04 / Pass or call
What goes wrong when a function is called instead of passed?
Writing a function’s name passes the function. Writing a call, with parentheses, runs the function at once and passes its result. Mixing the two up is the most common mistake with higher-order functions, and it goes two ways.
Calling too early: describe(addBonus(8)) runs addBonus before describe starts and passes the number 10. TypeScript refuses it: Argument of type 'number' is not assignable to parameter of type '(points: number) => number'.
Forgetting the call is worse, because TypeScript accepts it in some places. "8 becomes " + rule joins text with the function itself, and JavaScript turns a function into its code. The program runs and prints that code. In an if condition, TypeScript does catch a function that is never called: This condition will always return true since this function is always defined. Did you mean to call it instead?
function describe(rule: (points: number) => number): string {
return "8 becomes " + rule(8);
}
console.log(describe(addBonus)); // 8 becomes 10
// describe(addBonus(8)) does not compile.
// "8 becomes " + rule compiles and prints the code of addBonus. Pass f when the receiver will call it. Write f(x) only where the result itself is needed.
Coming from Python: Python prints <function add_bonus at 0x…> for a function value. JavaScript prints the function’s code, which can look like intended text.
Experiment
Leave out the call inside describe
Without parentheses, rule is the function itself, not its result. + with a string turns the other value into text, and JavaScript turns a function into its code, without the types. TypeScript allows + between a string and any value, so nothing reports the mistake.
Why it matters When printed text shows a function’s code, a call is missing. Look for a function name with no parentheses after it.
Predict the printed line. Then change rule(8) to rule in describe, and predict what is printed. Does TypeScript accept the change?
function describe(rule: (points: number) => number): string {
return "8 becomes " + rule(8);
}
function addBonus(points: number): number {
return points + 2;
}
console.log(describe(addBonus));
What to look for
describe lists rule as function addBonus(points). At line 2, the call rule(8) puts addBonus on top of describe, with points 8. The run prints 8 becomes 10. After the edit, TypeScript accepts the program, addBonus never runs, and the first printed line is 8 becomes function addBonus(points) {, followed by the rest of the function’s code on two more lines.
Expected output
8 becomes 1005 / Try it
Can you follow behavior passed in and returned?
One function is passed, one is built by a factory, and one call returns a function. Decide when each one runs.
function applyTwice(
rule: (points: number) => number,
points: number,
): number {
return rule(rule(points));
}
function makeScorer(difficulty: number): (points: number) => number {
return (points) => points * difficulty;
}
function addBonus(points: number): number {
return points + 2;
}
const a = applyTwice(addBonus, 5);
const b = applyTwice(makeScorer(2), 3);
console.log(a, b);
What is printed?
Reveal the reasoning
9 12. applyTwice calls the rule it receives two times, the second time on the first result. addBonus turns 5 into 7, then 9. makeScorer(2) runs once, before applyTwice starts, and its result is a function that doubles: 3 becomes 6, then 12.
Practice tickets
- HOF-101 · The board shows each athlete’s worst result as their best → Free
- HOF-102 · Every win counts as a first-round win → Sign in · Pro
- HOF-103 · Practice marks fall below zero or stay above the scale → Sign in · Pro