RizTech Academy logo
RizTech Academy
Objects and ArraysLesson 3 of 730 min

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

  1. map the orders to an array of customer names, then to an array of totals.
  2. Write the same transformation as a for...of loop with push, then decide which you would rather read in six months.
  3. Get the undefined array. Write a map with braces and no return, then fix it both ways.
  4. filter the unpaid orders. Then filter for something that matches nothing and confirm you get [] rather than undefined.
  5. Write if (results) on an empty filter result and watch it run when it should not. Fix it with .length.
  6. Use reduce to total the plates. Then do it again without the starting value on an empty array and read the error.
  7. Use reduce to find the largest order. Then do the same with Math.max(...plates) and decide which is clearer.
  8. Group the orders by paid status into { paid: [...], unpaid: [...] }.
  9. Chain filter, map and reduce to total only the paid orders. Read your chain aloud as a sentence.
  10. Harder. Given the orders, produce a summary object: { customers: 3, totalPlates: 8, revenue: 640, unpaid: ['Arjun'] } — in a single reduce. 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