A 32-minute working session on the functional toolkit every developer can use today — pure functions, immutability, map/filter/reduce, composition, closures, and modeling absence with Option/Result — framed for JavaScript and TypeScript, with honest notes on when the simpler option wins.
Functional programming is less about a language and more about a habit: build your logic out of small pure functions — pieces that take values in and hand values back, and do nothing sneaky in between. Get that right and your code becomes predictable, testable, and safe to move around. It's the paradigm cousin of OOP & Architecture; most real codebases blend the two.
The pure function only maps inputs to an output. The impure one also pokes at a global and does I/O — the dashed arrows are the side effects.
let total = 0 // shared state function addToCart(n: number) { total += n // mutates the outside world console.log(total) // I/O side effect } // call it twice, get different results — hard to test
function addToCart(cart: number, n: number): number { return cart + n // nothing outside is touched } addToCart(10, 5) // 15 — always, no matter when // trivially testable: feed inputs, check the output
Like a vending machine — press B4, get the same snack every time. A function that depends on the weather outside isn't a vending machine; it's a mood.
The single biggest source of "but it worked a second ago" bugs is shared mutable state: two parts of your program hold the same object, one quietly edits it, and the other breaks. The functional fix is immutability — treat values as read-only and build a new value instead of editing the old one.
Top: two names point at one array, so a mutation leaks. Bottom: the update returns a new array and the original is left intact.
Immutability removes the whole class of bug: if nothing can be edited in place, no one can edit it out from under you.
const prices = [10, 20] function applyTax(arr: number[]): number[] { arr.push(arr[0] * 0.2) // edits the original! return arr } applyTax(prices) // prices is now [10, 20, 2] — a surprise mutation
const prices = [10, 20] function withTax(arr: number[]): number[] { return [...arr, arr[0] * 0.2] // fresh copy } const taxed = withTax(prices) // prices stays [10, 20]; taxed is the new value
A higher-order function takes a function as an argument (or returns one). That one idea unlocks the workhorses of everyday FP: map, filter, and reduce — three building blocks that replace most hand-written loops with code that reads like the requirement.
Run a function over every element and collect the results. Same length out, each item reshaped.
[1,2,3].map(n => n * 2) // [2, 4, 6]
Keep only the elements where the test returns true. Same items, fewer of them.
[1,2,3,4].filter(n => n % 2 === 0) // [2, 4]
Walk the list carrying an accumulator, combining as you go. Many in, one value out.
[1,2,3].reduce((a,b) => a+b, 0) // 6
map keeps the count and reshapes; filter trims; reduce collapses the whole list into a single value.
Because each returns a value, you can pipe them together. The chain reads top-to-bottom like a sentence — and there's no index, no temporary array, no off-by-one to get wrong.
const out: number[] = [] for (let i = 0; i < orders.length; i++) { if (orders[i].paid) out.push(orders[i].total) } let sum = 0 for (let i = 0; i < out.length; i++) sum += out[i] // correct, but the intent is buried in plumbing
const sum = orders .filter(o => o.paid) // keep some .map(o => o.total) // transform each .reduce((a, b) => a + b, 0) // fold to one // reads like the requirement out loud
Like a kitchen line — one station slices (map), one tosses out the bad pieces (filter), one plates it all into a single dish (reduce).
If pure functions are the bricks, composition is the mortar: wire small functions together so the output of one feeds the input of the next. Currying and partial application are the techniques that make functions easy to wire — by letting you supply arguments a few at a time.
compose(f, g)(x) means f(g(x)); pipe is the same idea read left-to-right, which usually matches how we narrate the steps.const compose = (f: Function, g: Function) => (x: unknown) => f(g(x)) const pipe = (...fns: Function[]) => (x: unknown) => fns.reduce((v, f) => f(v), x) const clean = pipe(trim, toLower, dropAccents) clean(" Café ") // "cafe" // each step is tiny, pure, and reusable
A pipe is an assembly line for data: the value flows through each small function in order.
Currying rewrites a multi-argument function as a chain of one-argument functions: add(a, b) becomes add(a)(b). Partial application is what that buys you — call it with some arguments now and get back a specialized function that remembers them, waiting for the rest.
add(1) is an increment; discount(0.5) is "half off".// curried: one argument at a time const add = (a: number) => (b: number): number => a + b const inc = add(1) // partial application inc(10) // 11 const discount = (rate: number) => (price: number): number => price * (1 - rate) const half = discount(0.5) // "half off" tool half(80) // 40
Supply some arguments now; get back a ready-made function that remembers them for later.
Two mechanics make the functional toolkit work. A closure lets a function carry private state without a class. Recursion lets a function express "do this, then do it to the rest" without a mutable loop counter. Both lean on the same idea: a function is a value you can capture, pass, and reuse.
function counter(): () => number { let n = 0 // captured by the inner fn return () => ++n // closes over n } const next = counter() next() // 1 next() // 2 — n survives between calls, stays private
next holds onto the n from its birth scope — nobody else can see or touch it.
function sum(list: number[]): number { if (list.length === 0) return 0 // base case const [head, ...rest] = list return head + sum(rest) // recursive case } sum([1, 2, 3]) // 6
Each call peels off the head and defers the rest, until the base case returns 0 — then the additions resolve on the way back up.
Two everyday things blow up programs: a value that might be missing, and an operation that might fail. The imperative answers — null and throw — are invisible: nothing in a function's signature warns you they can happen. The functional answer makes both explicit in the return type, so the compiler forces you to handle them.
Some(x) or None — replaces null. Result (a.k.a. Either) — either Ok(value) or Err(error) — replaces a thrown exception. Both turn an invisible possibility into a visible branch you can't forget to handle.The return type itself spells out both outcomes — the "nothing" and the "failed" paths are right there in the signature.
null and exceptions don't show up in a type; an Option or Result does. The compiler becomes your checklist.function findUser(id: number) { const u = db.get(id) return u ?? null // signature hides the null } findUser(7).name // crashes if it was null // nothing forced the caller to check
type Result<T, E> = | { ok: true; value: T } | { ok: false; error: E } function parsePrice(s: string): Result<number, string> { const n = Number(s) return Number.isNaN(n) ? { ok: false, error: "not a number" } : { ok: true, value: n } }
Like a parcel that arrives marked either "contents inside" or "delivery failed: reason" — you open it knowing both outcomes exist, instead of being surprised by an empty box.
FP and OOP are not enemies; they answer different questions. OOP bundles state with the behavior that guards it; FP keeps data and behavior separate and leans on pure transformations. Most strong codebases in 2026 are hybrids — an object-oriented skeleton with a functional core.
OOP draws a boundary around data and its methods; FP keeps data plain and pushes it through a chain of pure functions.
You don't need a new language to write functional code. Pick on how much immutability help you actually need.
Choose as the default for everyday transforms — reach for a library only when this falls short.
map/filter/reduce everyone already reads, zero dependencies.Choose for deep, nested state updates (Redux / React reducers).
Choose when you genuinely lean into currying/composition across a codebase.
Worth studying even if you never ship them — each pushes one functional idea to its limit.
Choose to learn FP deeply or for type-driven domains.
Choose for highly concurrent, resilient backend services.
Choose for data-heavy, REPL-driven work on the JVM.
Choose for functional code that still needs the .NET ecosystem.
for loop is sometimes clearer than a clever three-step chain — readability wins.Five quick questions on purity, immutability, higher-order functions, closures, and Option/Result — instant feedback, no sign-in.
Navigate with ← → or scroll · back to library