Higher-Order Functions

Here is an idea that quietly reshapes how you write code: a function can be a value. Just like a number or a string, you can store it in a variable, pass it into another function, or return it as a result. A function that takes a function as an argument, or returns one, is called a higher-order function.

Why care? Because a huge amount of everyday code is really "walk through a list and do something to each item." Higher-order functions let you write the walking once, for all time, and just hand in the doing. Three of them — map, filter, and reduce — cover an astonishing fraction of all list processing you'll ever do.

Passing a function as an argument

Let's make it concrete. \text{applyTwice} knows nothing about what to do — it just does the given thing twice. The behaviour is chosen by the function you pass in.

function applyTwice(f: (x: number) => number, start: number): number { return f(f(start)); // do f, then do f again } const addOne = (x: number) => x + 1; const triple = (x: number) => x * 3; console.log(applyTwice(addOne, 10)); // 10 -> 11 -> 12 console.log(applyTwice(triple, 2)); // 2 -> 6 -> 18

Same machine, two totally different results — because the function itself was the input. That flexibility is the whole game.

map — transform every item

map takes a list and a function, and returns a new list where each item has been run through the function. The list stays the same length; each element is transformed. It replaces the classic "make an empty array, loop, push" ritual with one line.

const nums = [1, 2, 3, 4, 5]; const squares = nums.map((x) => x * x); console.log("squares:", squares); // [1, 4, 9, 16, 25] const labels = nums.map((x) => "#" + x); console.log("labels:", labels); // ["#1", "#2", ...] console.log("original untouched:", nums); // map is pure — nums unchanged

Notice map is pure: it returns a fresh array and never disturbs the original.

filter — keep only what you want

filter takes a predicate — a function that returns true or false — and returns a new list of just the items for which it said true. The list can come out shorter; nothing gets transformed.

const nums = [7, 2, 9, 4, 11, 6, 3]; const evens = nums.filter((x) => x % 2 === 0); console.log("evens:", evens); // [2, 4, 6] const big = nums.filter((x) => x > 5); console.log("bigger than 5:", big); // [7, 9, 11, 6]

reduce — boil a list down to one value

reduce is the powerful one. It walks the list carrying an accumulator — a running result — and on each item combines the accumulator with that item to make the next accumulator. When the list runs out, the accumulator is your answer. You give it two things: the combining function (acc, x) => ... and a starting value.

const nums = [3, 1, 4, 1, 5, 9]; // sum: start at 0, keep adding const total = nums.reduce((acc, x) => acc + x, 0); console.log("sum:", total); // 23 // max: start at the first-ish value, keep the bigger const biggest = nums.reduce((acc, x) => Math.max(acc, x), nums[0]); console.log("max:", biggest); // 9 // reduce is a Swiss army knife — even map & filter can be built from it const doubled = nums.reduce((acc, x) => [...acc, x * 2], [] as number[]); console.log("doubled via reduce:", doubled);

The starting value seeds the accumulator and, crucially, decides the answer for an empty list: summing nothing should give 0, multiplying nothing should give 1. That seed is the operation's identity element — the value that changes nothing. Leave it out and reducing an empty array throws an error, which is exactly the kind of edge-case bug a good starting value prevents.

Chaining them: a tiny data pipeline

Because each returns a new list, you can chain them into a readable pipeline that reads like a sentence: take the numbers, keep the evens, square them, add them up.

const answer = [1, 2, 3, 4, 5, 6] .filter((x) => x % 2 === 0) // [2, 4, 6] .map((x) => x * x) // [4, 16, 36] .reduce((acc, x) => acc + x, 0); // 56 console.log("sum of squares of evens:", answer);

Compare that to the equivalent tangle of a loop with an if, a temporary array, and an accumulator. The higher-order version says what you want, not how to shuffle indices — that's the declarative payoff.

The callbacks you pass to map/filter/reduce should be pure. It's tempting to reach for map when you really mean "do a side effect for each item" — but that's a misuse:

Rule of thumb: transforming data → map/filter/reduce; poking the outside world → forEach.