Functional Patterns & Immutability
Advanced · 18 min read · ▶ live playground · ✦ checkpoint
The easiest bug to miss is the one you create while "just updating" data. One function changes a player object, another function reads it later, and suddenly yesterday's score has moved. Functional patterns help you write transformations that are predictable: the same input gives the same output, and old data stays where you left it.
Pure functions and side effects
A pure function is a function that returns the same result for the same inputs and does not change anything outside itself. Think of it as a sealed recipe card: give it the same ingredients, and it hands back the same dish without scribbling on the pantry labels.
A side effect is any change a function makes outside its return value, such as mutating an object, writing to storage, starting a timer, or printing to the console. Side effects are not evil. Programs need them at the edges. You just want to know where they are.
const player = {
name: "Mina",
score: 10,
};
function addBonusPure(playerCard, bonus) {
return {
...playerCard,
score: playerCard.score + bonus,
};
}
function addBonusWithSideEffect(playerCard, bonus) {
playerCard.score = playerCard.score + bonus;
return playerCard;
}
const preview = addBonusPure(player, 5);
console.log(player.score);
console.log(preview.score);
addBonusWithSideEffect(player, 5);
console.log(player.score);→ 10, 15, then 15.
The pure function returns a new player card and leaves the original alone. The side-effect function edits the object it received. Both are legal JavaScript. The first is easier to test because you can call it, check its return value, and stop worrying about hidden damage.
console.log is a side effect too: it writes output. In lessons, we use it at the edge so you can see results. Inside your data transformations, keep the core work pure when you can.
Currying and partial application
Currying means writing a function that takes one argument and returns another function for the next argument. Partial application means pre-filling some arguments now so the returned function is ready to use later. The analogy is a pre-filled order slip: you choose the house sauce once, then hand the slip to every sandwich order.
function atLeast(minimumScore) {
return (player) => player.score >= minimumScore;
}
function addLabel(prefix) {
return (player) => `${prefix}: ${player.name} (${player.score})`;
}
const players = [
{ name: "Mina", score: 18 },
{ name: "Rae", score: 9 },
{ name: "Sol", score: 14 },
];
const isFinalist = atLeast(12);
const finalistLabel = addLabel("Finalist");
const labels = players
.filter(isFinalist)
.map(finalistLabel);
console.log(labels);→ ['Finalist: Mina (18)', 'Finalist: Sol (14)'].
atLeast(12) does not check a player yet. It creates a checker. addLabel("Finalist") creates a labeler. That makes array pipelines read like small named decisions instead of a pile of inline logic.
You do not need to curry everything. Reach for it when one choice gets reused: a minimum score, a tax rate, a category name, a formatting style. If the function is only called once, a regular function with two parameters is usually clearer.
Composition builds pipelines
Composition means building a bigger operation by connecting smaller functions. A pipeline is a composition that reads in order, with each step handing its result to the next step. Picture a scorecard moving across a few desks: one desk adds a bonus, one desk marks the mood, and the last desk prints the label.
function pipe(...steps) {
return (startingValue) =>
steps.reduce((currentValue, step) => step(currentValue), startingValue);
}
const addPracticeBonus = (player) => ({
...player,
score: player.score + 3,
});
const markReady = (player) => ({
...player,
mood: "ready",
});
const toScoreLine = (player) => `${player.name}: ${player.score} (${player.mood})`;
const prepareScoreLine = pipe(addPracticeBonus, markReady, toScoreLine);
console.log(prepareScoreLine({ name: "Rae", score: 11 }));→ Rae: 14 (ready).
Each step has one job and returns a value for the next step. No step needs to know the whole story. That is the same composition-first idea from 11.4, now with functions instead of helper objects.
Good composition depends on boring function shapes. A step that receives one value and returns one value is easy to chain. A step that mutates three outside variables, reads a global setting, and sometimes prints is hard to reuse because it drags its whole room along.
Try it — refactor a noisy transform
Run this once, then change atLeast(10) to atLeast(15). The pipeline should need no other edits because each function does one small job.
Immutable update patterns
Immutability means treating existing data as unchanging after creation. You already saw why this matters in 7.4: object variables are reference tickets, and aliases can change the same object from two names.
An immutable update creates new containers for the parts that change while reusing untouched parts. Think of it as filling out a new scorecard page instead of erasing the old one. Anyone holding the old page still sees the old score.
const profile = {
name: "Mina",
stats: {
score: 18,
streak: 3,
},
badges: ["starter"],
};
const nextProfile = {
...profile,
stats: {
...profile.stats,
score: profile.stats.score + 5,
},
badges: [...profile.badges, "focus"],
};
console.log(profile.stats.score);
console.log(nextProfile.stats.score);
console.log(profile.badges.join(", "));
console.log(nextProfile.badges.join(", "));
console.log(profile === nextProfile);
console.log(profile.stats === nextProfile.stats);→ 18, 23, starter, starter, focus, false, then false.
The pattern is "copy the path you change." The top-level profile changes, so you spread profile. The nested stats object changes, so you spread profile.stats. The badges array changes, so you create a new array with [...profile.badges, "focus"].
You do not need structuredClone for every update. Deep cloning the whole object can hide what changed and can waste work. Most framework-style state updates use shallow copies along the changed path because it keeps identity meaningful: unchanged branches keep the same reference, changed branches get a new one.
Frameworks later in the course build on this exact idea. They often decide what to redraw by checking whether a value's reference changed. You do not need the framework yet; you need the habit: return new data instead of mutating old data in place.
Checkpoint
Answer all three to mark this lesson complete
Functional patterns are not a separate language. They are JavaScript functions, arrays, and objects used with discipline: small transformations in, new values out. Next, you switch from shaping data to reading patterns in text with regular expressions.