map, filter and reduce
Three methods, all taking a callback, which between them replace most of the loops you have written so far. They are not shorter for the sake of it — each one announces what the loop is for, so a reader knows the shape of the answer before reading the body.
Everything here operates on this:
const orders = [
{ customer: 'Priya', plates: 3, paid: true },
{ customer: 'Arjun', plates: 0, paid: false },
{ customer: 'Meera', plates: 5, paid: true },
];
map: same number of things, transformed
const totals = orders.map((order) => order.plates * 80);
console.log(totals);
[ 240, 0, 400 ]
Three orders in, three numbers out. map always returns an array the same
length as the one it started with. If you find yourself wanting fewer, you
want filter.
Compare the loop it replaces:
const totals = [];
for (const order of orders) {
totals.push(order.plates * 80);
}
Same result, four lines, and a reader has to reach push before knowing what is
being built. map says it at the start.
Extracting one field is the commonest use:
console.log(orders.map((order) => order.customer));
[ 'Priya', 'Arjun', 'Meera' ]
The trap, from module 3: braces need an explicit return.
console.log(orders.map((order) => { order.customer; }));
[ undefined, undefined, undefined ]
If a map gives you an array of undefined, this is why, every time.
filter: fewer things, unchanged
const paid = orders.filter((order) => order.paid);
console.log(paid.map((order) => order.customer));
[ 'Priya', 'Meera' ]
The callback must return a boolean-ish value, and filter keeps the items
where it was truthy. The items themselves are untouched — filter selects, it
does not transform.
No matches gives an empty array, not undefined:
console.log(orders.filter((order) => order.plates > 99));
[]
That is convenient — you can map over it safely — but remember an empty array
is truthy, from module 2. if (results) is always true. Check
results.length.
filter versus find: filter gives you every match as an array; find
gives you the first match itself. If you want one thing, use find —
filter(...)[0] does more work and reads worse.
reduce: many things, one result
reduce is the one people avoid. It is the most powerful of the three and worth
the ten minutes.
const totalPlates = orders.reduce((running, order) => running + order.plates, 0);
console.log(totalPlates);
8
Two arguments: the callback, and the starting value — the 0 at the end,
which is easy to miss and important.
The callback gets the running result and the current item, and whatever it returns becomes the running result for the next item. Step by step:
| Item | running in |
returns |
|---|---|---|
| start | 0 |
|
| Priya, 3 | 0 |
3 |
| Arjun, 0 | 3 |
3 |
| Meera, 5 | 3 |
8 |
So 8.
Always pass the starting value. Without it, reduce uses the first item as
the starting point, and on an empty array there is no first item:
console.log([].reduce((a, b) => a + b));
TypeError: Reduce of empty array with no initial value
console.log([].reduce((a, b) => a + b, 0));
0
An empty list is exactly the case you did not test, so make the habit automatic.
reduce is not only for sums
The starting value can be any shape. An object, to count things:
const byStatus = orders.reduce((counts, order) => {
const key = order.paid ? 'paid' : 'unpaid';
counts[key] = (counts[key] || 0) + 1;
return counts;
}, {});
console.log(byStatus);
{ paid: 2, unpaid: 1 }
Note the return counts — with braces, forgetting it gives undefined on the
next pass and a confusing error. And (counts[key] || 0) handles the first time
a key is seen.
Group items into buckets — the pattern you will use most:
const grouped = orders.reduce((groups, order) => {
const key = order.paid ? 'paid' : 'unpaid';
groups[key] = groups[key] || [];
groups[key].push(order.customer);
return groups;
}, {});
console.log(grouped);
{ paid: [ 'Priya', 'Meera' ], unpaid: [ 'Arjun' ] }
Chaining
The three combine, and reading left to right tells you the whole story:
const paidTotal = orders
.filter((order) => order.paid)
.map((order) => order.plates * 80)
.reduce((sum, amount) => sum + amount, 0);
console.log(paidTotal);
640
"Keep the paid ones, turn each into money, add it up." That is a sentence, and the code is in the same order as the sentence.
Name the cost honestly. Each step builds a new array, so that chain makes two
intermediate arrays for one number. For the sizes you will meet in a browser —
hundreds, thousands — this is irrelevant and clarity wins easily. For hundreds of
thousands in a tight loop, one pass with reduce or a plain for...of is
faster. Write the clear version first; measure before changing it.
Which one
| You want | Use |
|---|---|
| The same number of items, changed | map |
| Fewer items, unchanged | filter |
| One value from many | reduce |
| The first match | find |
| Yes or no about the whole list | some / every |
| To do something with each, returning nothing | forEach or for...of |
forEach deserves a note: it returns undefined, so it cannot be chained,
and you cannot break out of it. When you need to stop early, use for...of.
Most forEach calls are really a map that has forgotten to return something.
Check your work
orders.map((o) => o.plates * 80) is [240, 0, 400] — three in, three out,
including the zero.
A map callback with braces and no return gives [undefined, undefined, undefined].
orders.filter((o) => o.paid) keeps Priya and Meera. filter selects
without transforming.
A filter matching nothing gives [], which is truthy. Test .length.
orders.reduce((r, o) => r + o.plates, 0) is 8 — 3 + 0 + 5.
[].reduce((a, b) => a + b) throws TypeError: Reduce of empty array with no initial value. With , 0 it returns 0. Always pass the starting value.
Grouping with reduce gives { paid: [ 'Priya', 'Meera' ], unpaid: [ 'Arjun' ] }. The groups[key] = groups[key] || [] line is what handles a key
being seen for the first time, and the return groups is what makes the next
pass work.
The chained paid total is 640 — Priya's 3 and Meera's 5 make 8 plates at
₹80.
forEach returns undefined and cannot be chained or broken out of.
The single-reduce summary:
const summary = orders.reduce(
(acc, order) => {
acc.customers += 1;
acc.totalPlates += order.plates;
acc.revenue += order.plates * 80;
if (!order.paid) acc.unpaid.push(order.customer);
return acc;
},
{ customers: 0, totalPlates: 0, revenue: 0, unpaid: [] },
);
{ customers: 3, totalPlates: 8, revenue: 640, unpaid: [ 'Arjun' ] }
One pass, and the starting value documents the shape of the answer before you
read the body. The four-chain version is easier to read line by line but walks
the array four times and repeats orders. four times. For three orders, take
the chain; for a summary with four fields that always travel together, take the
reduce. The honest answer is that the chain is usually right and this is one of
the cases where it is not.
Practice
mapthe orders to an array of customer names, then to an array of totals.- Write the same transformation as a
for...ofloop withpush, then decide which you would rather read in six months. - Get the undefined array. Write a
mapwith braces and noreturn, then fix it both ways. filterthe unpaid orders. Then filter for something that matches nothing and confirm you get[]rather thanundefined.- Write
if (results)on an empty filter result and watch it run when it should not. Fix it with.length. - Use
reduceto total the plates. Then do it again without the starting value on an empty array and read the error. - Use
reduceto find the largest order. Then do the same withMath.max(...plates)and decide which is clearer. - Group the orders by paid status into
{ paid: [...], unpaid: [...] }. - Chain
filter,mapandreduceto total only the paid orders. Read your chain aloud as a sentence. - Harder. Given the orders, produce a summary object:
{ customers: 3, totalPlates: 8, revenue: 640, unpaid: ['Arjun'] }— in a singlereduce. Then write the same thing as four separate chained operations. Decide which you would hand to a colleague, and say why.
Next: destructuring and spread — the syntax that has been quietly appearing in these examples since module 2.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship