So far you have written programs as a list of instructions: set this variable, loop over that list, change this counter. That style has a name — imperative programming — and it works by commanding the machine, step by step, to change its state. Functional programming is a different way of thinking. Instead of telling the computer how to shuffle data around, you describe the answer as a combination of functions: take these values, feed them through this function, then that one, and read off the result.
Two ideas sit at its heart, and this page is really about getting comfortable with both:
The pay-off is code that is far easier to reason about: if a function always gives the same answer for the same inputs and never quietly changes anything elsewhere, you can trust it in isolation — and the computer can even run many such functions in parallel without them treading on each other.
Here is the idea that surprises everyone first. In TypeScript a function is a value, just
like 7 or "hello". That means you can store one in a variable, and you
can hand a function to another function as an argument. Run this:
Look at applyTwice: its first parameter, f, is a function.
We passed in double once and square the next time, and the same little
machine did two completely different jobs. A function that takes a function (or
returns one) is called a higher-order function — and they are the engine
of the whole functional style.
Yes — and it's wonderfully useful. A function that returns a function lets you make
customised functions on demand. Here multiplyBy takes a number and returns
a brand-new function that multiplies by it:
multiplyBy(3) handed us a function that will forever multiply by 3. We made
triple and tenTimes from one recipe — the function manufactured
more functions.
map, filter, reduceOnce functions can be passed around, three higher-order functions on arrays do almost all the everyday work — and you'll meet them in every functional language. Each takes a function and applies it across a list, without you writing a loop:
map — transform every item, giving a new list the
same length (marks → doubled marks);filter — keep only the items that pass a test, giving a
shorter list (marks → just the passes);reduce — combine the whole list down to a
single value (marks → their total).
Here they are on a class's exam marks: double every mark, keep only the passes (≥ 50), then add
them into one total. Notice how each step reads like a sentence, and how the original
marks array is never changed:
The same three, chained into a single pipeline — data flowing left to right through each transformation. This is the classic functional look, and it does exactly what the three separate steps above did:
Read that pipeline aloud: "take the marks, double each, keep the passes, add them up." You described what you want, not the bookkeeping of indices and counters an imperative loop would need.
reduce carries an accumulator — a running result — and updates it
once per item. You give it a combining function (acc, item) => newAcc and a
starting value. For summing [60, 90, 80] starting at
0:
Change the combining function and reduce does something else entirely: use
(acc, m) => Math.max(acc, m) to find the biggest mark, or
(acc, m) => acc + 1 to count them. reduce is the general tool —
map and filter are just its two most common special cases.
You already know how to total the passing marks the imperative way. It's not wrong — but
watch how much machinery it needs: a mutable accumulator, an explicit loop, an
if, and a variable you keep re-assigning:
Same result, but the imperative version spends its words telling the machine how to walk the list. The functional pipeline states what the answer is. Neither is "better" everywhere — but the functional style tends to be shorter, harder to get subtly wrong (no off-by-one index bugs), and easier to read once the vocabulary clicks.
A pure function obeys two simple promises:
(n) => n * 2 is pure: feed it 5 and you get 10, today,
tomorrow, and on every machine. A pure function is like a mathematical function — a reliable box
that maps inputs to outputs and does nothing sneaky. That reliability is why functional
code is easy to trust: to understand a pure function you only need to look at the function itself,
never the rest of the program.
The other half of the discipline is leaving existing data alone. Rather than
editing an array or object in place, you build a fresh one. That's exactly why
map and filter hand you a new array and leave the original
intact — they are immutable by design.
Compare a mutating approach with an immutable one for "add a new mark to the list":
Why bother? If nobody ever changes marks out from under you, then any code holding it
can rely on it staying put — no "who changed my list?" mysteries. And because independent pieces of
work never share a value they might both edit, the computer can safely run them
in parallel. Immutability and purity are what make functional code so friendly to
multi-core machines.
The classic trap is an impure function that looks innocent. A function that changes a global variable, or mutates its argument, is impure — and impure functions are far harder to trust, because their result now depends on hidden state and they leave damage behind. Look at these two:
Both break the pure-function promises. The fix is always the same: depend only on your
inputs, and return a new value instead of mutating. A pure addBonus would
write return scores.map((s) => s + 10); — leaving the caller's array exactly as
it found it. When a function surprises you, ask first: "is it secretly changing something it
shouldn't?"