Functional Programming

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.

Functions are values

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:

// store a function in a variable, exactly like storing a number const double = (n: number): number => n * 2; const square = (n: number): number => n * n; // a function that TAKES a function and uses it function applyTwice(f: (x: number) => number, start: number): number { return f(f(start)); // call the passed-in function on its own result } console.log(applyTwice(double, 3)); // double(double(3)) = 12 console.log(applyTwice(square, 3)); // square(square(3)) = 81

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:

function multiplyBy(factor: number): (n: number) => number { return (n: number) => n * factor; // a fresh function that remembers `factor` } const triple = multiplyBy(3); const tenTimes = multiplyBy(10); console.log(triple(5)); // 15 console.log(tenTimes(5)); // 50

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.

The big three: map, filter, reduce

Once 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:

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:

const marks: number[] = [12, 30, 45, 8, 40]; // map: transform every mark → a new same-length array const doubled = marks.map((m) => m * 2); console.log("doubled:", doubled); // [24, 60, 90, 16, 80] // filter: keep only the passes (≥ 50) → a shorter array const passes = doubled.filter((m) => m >= 50); console.log("passes:", passes); // [60, 90, 80] // reduce: fold the list into ONE value — the running total const total = passes.reduce((sum, m) => sum + m, 0); console.log("total of passes:", total); // 230 console.log("original untouched:", marks); // [12, 30, 45, 8, 40]

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:

const marks: number[] = [12, 30, 45, 8, 40]; const total = marks .map((m) => m * 2) // double each .filter((m) => m >= 50) // keep the passes .reduce((sum, m) => sum + m, 0); // add them up console.log(total); // 230

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:

start: acc = 0 item 60: acc = 0 + 60 = 60 item 90: acc = 60 + 90 = 150 item 80: acc = 150 + 80 = 230 result: 230

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.

The imperative version — for contrast

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:

const marks: number[] = [12, 30, 45, 8, 40]; let total = 0; // a variable we will keep CHANGING for (let i = 0; i < marks.length; i++) { const doubled = marks[i] * 2; if (doubled >= 50) { total = total + doubled; // mutate the accumulator each pass } } console.log(total); // 230 — same answer

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.

Pure functions: same input, same output

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.

// PURE: depends only on its inputs, changes nothing outside function add(a: number, b: number): number { return a + b; } console.log(add(2, 3)); // 5 console.log(add(2, 3)); // 5 — and it will ALWAYS be 5

Immutability: make new data, don't mutate

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":

const marks: number[] = [40, 55, 70]; // MUTATING: push changes the original array in place const mutated = [40, 55, 70]; mutated.push(88); console.log("after push, original changed:", mutated); // [40, 55, 70, 88] // IMMUTABLE: build a NEW array, leave the old one untouched const extended = [...marks, 88]; // spread the old items into a new array console.log("new array:", extended); // [40, 55, 70, 88] console.log("original safe:", marks); // [40, 55, 70]

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:

let runningTotal = 0; // a global // IMPURE #1: reaches out and changes a global — same input, DIFFERENT output each call function addToTotal(n: number): number { runningTotal += n; // side effect! return runningTotal; } console.log(addToTotal(5)); // 5 console.log(addToTotal(5)); // 10 — same input, different answer! Can't trust it. // IMPURE #2: mutates the array it was given — the caller's data is wrecked function addBonus(scores: number[]): number[] { for (let i = 0; i < scores.length; i++) scores[i] += 10; // mutates the ARGUMENT return scores; } const original = [40, 50]; addBonus(original); console.log("caller's array was changed:", original); // [50, 60] — surprise!

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?"