Arrays and Objects
By the end of this lesson
Transform collections with map, filter and reduce.
Most frontend work is a list being turned into another list. Employees become rows. Expenses become a filtered set, then a total, then formatted text.
Three methods cover nearly all of it. filter selects, map transforms, and reduce combines a list into a single value. Each one returns something new and leaves the original list untouched, which turns out to matter more than it first appears.
const expenses = [
{ id: "X-1", description: "Train to Leeds", category: "travel", amount: 48.5, status: "approved" },
{ id: "X-2", description: "Monitor stand", category: "equipment", amount: 32, status: "draft" },
{ id: "X-3", description: "Client lunch", category: "travel", amount: 74.5, status: "approved" },
];
const approved = expenses.filter((expense) => expense.status === "approved");
const descriptions = approved.map((expense) => expense.description);
const total = approved.reduce((runningTotal, expense) => runningTotal + expense.amount, 0);
console.log(approved.length); // 2
console.log(descriptions); // ["Train to Leeds", "Client lunch"]
console.log(total); // 123- Each method takes a function, and calls it once for every item. The parameter — expense here — is that item. Name it after what it is, not x.
- filter keeps the items whose function returned true. Two of the three are approved, so the new array has two items.
- map returns a new array with one result per input item. Always the same length as what went in.
- reduce carries a value along the list. The 0 at the end is where it starts; runningTotal is what the previous call returned. Forgetting that starting value is the usual reduce bug.
- expenses is unchanged after all three. Each method built something new, which means you can safely run them again with different filters.
- One aside about money: amounts as decimal numbers are fine for display, and real financial code usually stores whole pence instead, because binary fractions cannot represent every decimal exactly. 0.1 + 0.2 is famously not 0.3.
const [firstExpense] = expenses;
const { description, amount } = firstExpense;
console.log(description, amount); // "Train to Leeds" 48.5
// A copy with one field changed — the original is untouched
const corrected = { ...firstExpense, description: "Train to Leeds (return)" };
// A copy with defaults underneath the real values
const withCurrency = { currency: "GBP", ...firstExpense };
// A longer list, without changing the old one
const withNewClaim = [
...expenses,
{ id: "X-4", description: "Keyboard", category: "equipment", amount: 45, status: "draft" },
];
console.log(expenses.length, withNewClaim.length); // 3 4- The first line pulls the first item out of the array by position. Destructuring is a shorthand for reading values out into names.
- The second pulls named properties out of the object. The names have to match the property names.
- Three dots is spread: copy everything from this object or array into the one being built.
- Order matters in an object spread. Anything after the spread overrides what came from it, so corrected gets the new description, and withCurrency uses the original's currency if it had one.
- The array version appends without touching the original. expenses still has three items.
- One limit to know about: spread copies one level deep. A nested object inside the copy is still the same object, so changing it changes both. For flat records like these that is fine, and for nested data it is a real trap.
The array methods worth knowing by name:
- filter
- A new array of the items that passed the test. Length is the same or shorter.
- map
- A new array with each item transformed. Length is always the same.
- reduce
- One value built from the whole list — a total, a count, or an object grouped by category. Give it a starting value.
- find
- The first matching item, or undefined. Use it when there is at most one.
- some / every
- True if any item matches, or if all of them do. Both stop as soon as the answer is known.
- sort
- Reorders in place, so copy first. Pass a comparison function — the default sorts as text, which puts 100 before 20.
- includes / indexOf
- Whether a value is present, and where. Straightforward for primitives, not for objects.
- forEach
- Runs a function per item and returns nothing. For effects only.
These methods chain, because each one returns a new array. Reading a chain out loud usually describes the requirement: take the expenses, keep the travel ones, keep the approved ones, take their amounts, add them up.
Chains are easier to follow than a loop that does four things at once, up to a point. Once a chain runs to six or seven steps, split it and give the intermediate results names — the names are documentation.
Summary
- filter selects, map transforms, reduce combines — and all three return something new
- forEach returns nothing, so use map when you want a value back
- Destructuring reads values out into names; spread copies into a new object or array
- Spread copies one level deep, so nested objects are still shared
- sort and reverse mutate in place — copy before sorting anything you were handed
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Summarise by category
Using the expenses array from this lesson, produce an object where each key is a category and each value is the total approved amount for it, like { travel: 123, equipment: 0 }.
Then write the highest-value approved claim to the console without reordering the original array.
Show solution
reduce with an object as the starting value is the standard way to group. The accumulator is the object being built, and each call adds to one of its keys.
The || 0 handles the first time a category is seen, when the key does not exist yet and reading it gives undefined. Adding to undefined would give NaN and silently poison the total.
For the highest claim, the copy is the important part. [...approved].sort(...) leaves the original order intact, so the directory list rendered from the same array does not jump about. reduce would also work and needs no copy at all.
const approved = expenses.filter((expense) => expense.status === "approved");
const totalsByCategory = approved.reduce((totals, expense) => {
totals[expense.category] = (totals[expense.category] || 0) + expense.amount;
return totals;
}, {});
console.log(totalsByCategory); // { travel: 123 }
// Sort a copy so the original order survives
const [highest] = [...approved].sort((a, b) => b.amount - a.amount);
console.log(highest.description, highest.amount); // "Client lunch" 74.5
// Or with no copy at all
const highestByReduce = approved.reduce((best, expense) =>
expense.amount > best.amount ? expense : best
);Think about it
Think about it
A function called renderExpenses calls expenses.sort() before building the rows. Everything works. Three months later someone reports that the employee list on the same page changes order after the expenses panel opens. What happened, and why was the original code so hard to suspect?
Show solution
sort mutated the shared array. Both features read from the same list, so rendering one silently rearranged the other. The function that appeared to do nothing but draw the screen also changed the data.
It was hard to suspect because the symptom and the cause are in different features, and nothing failed. There is no error, no wrong value, only an order nobody chose. Bugs like this are usually found by accident.
The fix is one character's worth of thought: sort a copy. The general principle is that a function which is only supposed to read should not change what it was given, and rendering code should read.
Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.