map, filter, reduce and friends
Transform arrays without loops using map, filter, reduce, find, findLast, some, every, flat and flatMap, and chain them into pipelines.
What you will learn
- Choose between map, filter and reduce for a task
- Find items with find, findIndex, findLast and findLastIndex
- Test arrays with some and every
- Flatten nested arrays with flat and flatMap and chain methods into a pipeline
On this page
The higher-order functions lesson previewed map, filter and reduce. This lesson covers them properly, with the other array methods that take a callback. Together they replace most loops that build a result. The fundamentals lesson on sequences and arrays describes the general operations. Here is how JavaScript spells them.
All of these methods call your function with (item, index, array). None of them changes the array, unless your callback does.
The examples in this lesson use the same list of orders.
map, filter, reduce
mapreturns a new array of the same length, with each item replaced by the callback’s result.filterreturns a new array with only the items for which the callback returns a truthy value.reducefolds the array into one value. The callback gets the running result (the accumulator) and the next item. The second argument ofreduceis the starting accumulator.
const orders = [ { id: 1, customer: "Ada", total: 40, paid: true }, { id: 2, customer: "Linus", total: 15, paid: false }, { id: 3, customer: "Grace", total: 90, paid: true }, { id: 4, customer: "Ada", total: 25, paid: true },];
const totals = orders.map((o) => o.total);console.log(totals);
const paid = orders.filter((o) => o.paid);console.log(paid.map((o) => o.id));[ 40, 15, 90, 25 ][ 1, 3, 4 ]reduce adds up the paid orders. With an object as the accumulator, it can also group them by customer:
const revenue = paid.reduce((sum, o) => sum + o.total, 0);console.log(revenue);
const byCustomer = orders.reduce((acc, o) => { acc[o.customer] = (acc[o.customer] ?? 0) + o.total; return acc;}, {});console.log(byCustomer);155{ Ada: 65, Linus: 15, Grace: 90 }map passes the index as a second argument, and forEach returns nothing:
console.log(orders.map((o, i) => `${i + 1}. ${o.customer}`));console.log(orders.forEach((o) => o.id));[ '1. Ada', '2. Linus', '3. Grace', '4. Ada' ]undefinedThe reduce that builds byCustomer uses an object as the accumulator, which shows that the result need not be a number. acc[o.customer] ?? 0 supplies a starting value the first time a customer appears, using the nullish operator from the null and undefined lesson.
forEach is for side effects. It returns undefined, so there is nothing to chain or assign. If you want a new array, use map.
Always give reduce a starting value
Without a second argument, reduce uses the first item as the starting value and begins at the second. That works for sums of numbers and fails in two ways.
console.log([1, 2, 3].reduce((a, b) => a + b));console.log([5].reduce((a, b) => a + b));console.log([].reduce((a, b) => a + b, 0));
try { [].reduce((a, b) => a + b);} catch (error) { console.log(error.name);}
const words = ["apple", "bob", "cat"];console.log(words.reduce((n, w) => n + w.length));console.log(words.reduce((n, w) => n + w.length, 0));650TypeErrorapple3311- On an empty array,
reducewithout a starting value throws aTypeError. With0it returns0. - When the result has a different type from the items, the first item is used as it is. The first sum of word lengths started from the string
"apple"and joined strings and numbers into"apple33". The second, with0, gives11.
Finding items
find returns the first item for which the callback is truthy, or undefined. findIndex returns its position, or -1. findLast and findLastIndex search from the end and return the same kinds of result.
const scores = [72, 95, 58, 88, 95];
console.log(scores.find((s) => s > 90));console.log(scores.findIndex((s) => s > 90));console.log(scores.findLast((s) => s > 90));console.log(scores.findLastIndex((s) => s > 90));console.log(scores.find((s) => s > 100));console.log(scores.findIndex((s) => s > 100));
console.log(scores.some((s) => s < 60));console.log(scores.every((s) => s >= 50));console.log(scores.every((s) => s >= 60));
console.log([].some((s) => s > 0), [].every((s) => s > 0));951954undefined-1truetruefalsefalse truesome asks whether any item passes, and every asks whether all items do. Both stop as soon as the answer is known. On an empty array some is false and every is true, because no item can fail the test. That surprises people once, then it makes sense.
When find returns undefined, you cannot tell “not found” from “found an item whose value is undefined”. If that matters, use findIndex.
flat and flatMap
flat(depth) merges nested arrays into the outer one. The default depth is 1. Infinity flattens everything.
flatMap(fn) is map followed by flat() of depth 1. It is useful when each item produces zero, one or several results: return an empty array to drop an item, or an array of several to expand it.
const nested = [1, [2, 3], [4, [5, [6]]]];console.log(nested.flat());console.log(nested.flat(2));console.log(nested.flat(Infinity));
const sentences = ["the cat", "sat down"];console.log(sentences.map((s) => s.split(" ")));console.log(sentences.flatMap((s) => s.split(" ")));
const lines = [ { name: "tea", tags: ["hot", "drink"] }, { name: "ice", tags: ["cold"] },];console.log(lines.flatMap((l) => l.tags));
const numbers = [1, 2, 3, 4];console.log(numbers.flatMap((n) => (n % 2 === 0 ? [n, n * 10] : [])));[ 1, 2, 3, 4, [ 5, [ 6 ] ] ][ 1, 2, 3, 4, 5, [ 6 ] ][ 1, 2, 3, 4, 5, 6 ][ [ 'the', 'cat' ], [ 'sat', 'down' ] ][ 'the', 'cat', 'sat', 'down' ][ 'hot', 'drink', 'cold' ][ 2, 20, 4, 40 ]Chaining into a pipeline
Because each method returns an array, you can chain them. Read a pipeline from top to bottom as a list of steps.
Here is a short report from the order list.
const orders = [ { id: 1, customer: "Ada", total: 40, paid: true }, { id: 2, customer: "Linus", total: 15, paid: false }, { id: 3, customer: "Grace", total: 90, paid: true }, { id: 4, customer: "Ada", total: 25, paid: true },];
const paid = orders.filter((o) => o.paid);const revenue = paid.reduce((sum, o) => sum + o.total, 0);const unpaidIds = orders.filter((o) => !o.paid).map((o) => o.id);const customers = [...new Set(orders.map((o) => o.customer))];const biggest = orders.reduce((best, o) => (o.total > best.total ? o : best));
console.log(`Paid orders: ${paid.length}`);console.log(`Revenue: ${revenue}`);console.log(`Unpaid ids: ${unpaidIds.join(", ")}`);console.log(`Customers: ${customers.join(", ")}`);console.log(`Biggest: order ${biggest.id} (${biggest.customer})`);Paid orders: 3Revenue: 155Unpaid ids: 2Customers: Ada, Linus, GraceBiggest: order 3 (Grace)[...new Set(...)] removes duplicates. Sets get their own lesson later in this module. The reduce that finds the biggest order uses no starting value on purpose: the first order is a fine starting candidate, and the array is known to be non-empty.
Keep a chain short enough to read. If a step needs a name to be understood, assign the intermediate result to a const and name it, as paid is named above.
Picking at random
Math.random() returns a number from 0 up to but not including 1, and a different one each time. Scale and round it to get what you need. To get a whole number from min to max inclusive, multiply by the size of the range, round down, and add min. A random item is then an array element at a random index.
function randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min;}
function pick(items) { return items[randomInt(0, items.length - 1)];}
const die = randomInt(1, 6);console.log(die >= 1 && die <= 6);
const colours = ["red", "green", "blue"];console.log(colours.includes(pick(colours)));truetrueBecause the numbers change on every run, the example prints whether the results are in range rather than the results themselves. The same trick tests your own code. Math.random is fine for games and quizzes. It is not suitable for passwords or tokens, and it cannot be given a seed to repeat a sequence.
To shuffle an array, walk it from the end and swap each item with one at a random earlier position. Work on a copy, so the original stays as it was.
function randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min;}
function shuffled(items) { const copy = [...items]; for (let i = copy.length - 1; i > 0; i--) { const j = randomInt(0, i); const temp = copy[i]; copy[i] = copy[j]; copy[j] = temp; } return copy;}
const cards = [1, 2, 3, 4, 5];const mixed = shuffled(cards);console.log(mixed.length, cards);console.log([...mixed].sort((a, b) => a - b));5 [ 1, 2, 3, 4, 5 ][ 1, 2, 3, 4, 5 ]Sorting with sort(() => Math.random() - 0.5) looks like a shorter shuffle, but it is biased, so use the swap loop.
Common pitfalls
- Forgetting
returnin a callback with braces.orders.map((o) => { o.total })returns an array ofundefined. Either addreturn, or drop the braces. - Using
mapfor side effects. If you do not use the new array, useforEachor afor...ofloop. - Using
reducewhen a simpler method works. Areducethat only builds a filtered or mapped array should befilterormap. - Calling
reduceon a possibly empty array without a starting value. It throws. - Passing a function with optional parameters directly, such as
map(parseInt). The index lands in the wrong parameter. - Changing items inside
maporfilter. The new array holds the same objects as the old one. The copying lesson and the references lesson in this module explain why that matters.
Exercises
Here is a list of exam marks:
const marks = [ { student: "Ada", subject: "maths", score: 92 }, { student: "Grace", subject: "maths", score: 78 }, { student: "Ada", subject: "art", score: 64 }, { student: "Linus", subject: "maths", score: 55 }, { student: "Grace", subject: "art", score: 88 },];Print a report with the number of marks of 60 or more, the average maths score, the students with a score above 90, whether every score is at least 50, and the student of the first mark below 60. Use filter, map, reduce, find and every. The output should be:
Passing: 4Maths average: 75Top students: AdaAll at least 50: trueFirst failing: LinusShow solution
const marks = [ { student: "Ada", subject: "maths", score: 92 }, { student: "Grace", subject: "maths", score: 78 }, { student: "Ada", subject: "art", score: 64 }, { student: "Linus", subject: "maths", score: 55 }, { student: "Grace", subject: "art", score: 88 },];
const passing = marks.filter((m) => m.score >= 60);const maths = marks.filter((m) => m.subject === "maths");const mathsAverage = maths.reduce((sum, m) => sum + m.score, 0) / maths.length;const top = marks.filter((m) => m.score > 90).map((m) => m.student);const failing = marks.find((m) => m.score < 60);
console.log(`Passing: ${passing.length}`);console.log(`Maths average: ${mathsAverage}`);console.log(`Top students: ${top.join(", ")}`);console.log(`All at least 50: ${marks.every((m) => m.score >= 50)}`);console.log(`First failing: ${failing.student}`);Passing: 4Maths average: 75Top students: AdaAll at least 50: trueFirst failing: Linusfilter with score >= 60 leaves four marks. The maths average filters the maths marks first, then uses reduce with a starting value of 0 and divides by how many there were. Top students come from a filter followed by a map that keeps only the name. every answers the yes-or-no question, and find returns the first matching mark, so its student is Linus.
Given const teams = [["Ada", "Grace"], ["Linus"], []], produce one array of names in upper case, in two ways: once with flat and map, and once with a single flatMap. Both should print [ 'ADA', 'GRACE', 'LINUS' ].
Show solution
const teams = [["Ada", "Grace"], ["Linus"], []];console.log(teams.flat().map((name) => name.toUpperCase()));console.log(teams.flatMap((team) => team.map((name) => name.toUpperCase())));[ 'ADA', 'GRACE', 'LINUS' ][ 'ADA', 'GRACE', 'LINUS' ]flat() joins the three inner arrays into one, and the empty one adds nothing. In the flatMap version, each team is turned into a list of upper-case names and the lists are joined, so the result is the same.
Summary
maptransforms every item,filterkeeps some, andreducecombines them into one value. Givereducea starting value.find,findIndex,findLastandfindLastIndexlocate an item.someandeverytest the whole array.flatremoves a level of nesting.flatMapmaps and flattens in one step, and can drop or expand items.- Chains read well when each step is small. Name intermediate results when a step needs explaining.
Math.random()gives a number from 0 to just under 1. Scale and floor it for whole numbers, and shuffle a copy with swaps.
Next: sorting, and copying arrays without changing the original.