← Fundamentals
Guided lesson 6 questions · 4 languages

Map, filter, and reduce

Three loops you no longer write, and what each one gives back.

map gives one new value for each element, filter keeps the elements that pass a test, and reduce combines all the elements into one value, starting from a start value. Each one replaces a loop you would otherwise write, and each one calls your function for the elements.

Before you begin

Why this matters

Most code that walks an array builds a new array, keeps some elements, or computes one total. Once you can read map, filter, and reduce, you can see at a glance which of the three a line does, and predict how often its function runs.

What this helps you reason about

  • Rewrite a loop as map, filter, or reduce, and back.
  • Predict how many times a callback runs, and with which arguments.
  • Spot a reduce with no start value, and a callback that receives more arguments than it expects.

A talent show counts the votes for each act. Adding the judges’ votes to every act, keeping the acts that reach the final, and adding up all the votes are three loops. TypeScript arrays have a method for each one: map, filter, and reduce.

Already know this? The short version
  • map(f) calls f once per element, in order, and returns a new array of the same length; filter(test) returns a new array of the elements for which test returned a truthy value. Neither changes the original.
  • reduce(f, start) calls f(resultSoFar, element) for each element and returns the last result, or start for an empty array; without start it begins from the first element and throws a TypeError on an empty array.
  • Callbacks receive (element, index, array), so "1,2,3".split(",").map(parseInt) gives [1, NaN, NaN].
  • Array methods run at once, one whole array at a time. Put filter before map when the test reads the original value, and after map when it reads the new one.

Go to the final check· Go to the practice tickets

Words in this lesson (7)
map
An operation that applies a function to every element of a collection and gives the results in a new collection: one result per element, in the same order.
callback
A function passed to another function, which calls it, for example once for each element of an array.
arrow function
A function written as a value, without a name, such as () => "stall " + i. The part before => lists its parameters, and the part after it is the value it returns.
filter
An operation that applies a test to every element of a collection and keeps the elements for which the test gives true, in their original order.
accumulator
A variable that collects a result step by step, such as a running total that a loop adds each item to.
fold
An operation that combines all elements of a collection into one value. It starts from a start value and applies a function to the result so far and each element in turn; for an empty collection it gives the start value.
TypeError
The error JavaScript throws when an operation gets a value it cannot work with, such as reading a property of undefined.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Map

What does map do to each element?

A loop that builds a new array from an old one has a fixed shape: make an empty array, visit each element, and push one new value. map writes that loop for you. You pass map a function, and map calls it once for each element, in order. The results go into a new array with as many elements as the old one. The old array does not change.

A function passed to another function, which then calls it, is a callback. Here the callback is the arrow function (v) => v + 10: it takes one act’s votes and returns them with the judges’ 10 votes added.

main.ts
const votes = [120, 85, 240, 60];

const byLoop: number[] = [];
for (const v of votes) {
  byLoop.push(v + 10);
}

const byMap = votes.map((v) => v + 10);
console.log(byMap); // [ 130, 95, 250, 70 ]

map gives one new value per element: the new array has the same length as the old one.

Coming from Python: Python’s map returns a lazy iterator; an array’s map returns a finished array at once.

Precisely: map passes three arguments

map calls the callback with the element, its index, and the array itself. A callback that names one parameter ignores the other two. A function whose second parameter means something else, such as parseInt, whose second parameter is the base of the number, receives the index there by mistake; the experiment below shows this.

Experiment

Watch map call its callback for each element

map calls its callback once for each element, in order, and passes three arguments: the element, its index, and the whole array. The arrow function uses only the first. parseInt uses its second parameter as the base of the number, so it receives parseInt("85", 1) and parseInt("240", 2), and neither text can be read in that base: NaN.

Why it matters Pass map a function that takes exactly the arguments you mean, usually an arrow function such as (text) => parseInt(text).

Predict how many times the arrow function on line 3 runs, and what votes holds. Then change typed.map((text) => parseInt(text)) to typed.map(parseInt) and predict again.

// split cuts the text at each comma.
const typed = "120,85,240".split(",");
const votes = typed.map((text) => parseInt(text));
console.log(votes);
What to look for

Step past line 2: typed shows ["120", "85", "240"]. On line 3 a new frame appears above <top level>, named arrow function (line 3), with text "120", then "85", then "240". It stops twice for each element; Node often stops twice on one line. votes then shows [120, 85, 240], and the run prints [ 120, 85, 240 ]. After the edit, no new frame appears, because parseInt is not written in your file, and votes shows [120, NaN, NaN].

Expected output

[ 120, 85, 240 ]

02 / Filter

Which elements does filter keep?

A loop that keeps some elements has another fixed shape: test each element, and push it only when the test passes. filter writes that loop. Its callback returns a boolean, and filter keeps each element for which the callback returns true, in the original order. The new array has as many elements as the old one, or fewer, possibly none. filter never changes an element: it keeps it or leaves it out.

main.ts
const votes = [120, 85, 240, 60];

const byLoop: number[] = [];
for (const v of votes) {
  if (v >= 100) {
    byLoop.push(v);
  }
}

const finalists = votes.filter((v) => v >= 100);
console.log(finalists); // [ 120, 240 ]

filter keeps or drops whole elements; the result has the same length or fewer elements.

Precisely: a truthy result is enough

filter keeps an element when the callback’s result is truthy, not only when it is true. A callback that returns a number keeps every element whose result is not 0 or NaN, and TypeScript accepts such a callback. Return a boolean to say what you mean.

03 / Reduce

How does reduce combine a collection into one value?

The third loop shape keeps one value, an accumulator, and updates it with each element: a running total starts at 0 and adds each act’s votes. An operation that does this for you is called a fold. In TypeScript the fold is the array method reduce.

reduce takes a callback and a start value. It calls the callback once per element with two arguments: the result so far and the element. What the callback returns becomes the result so far for the next element. After the last element, reduce returns the result. For an empty array the callback never runs, and reduce returns the start value.

The start value can be left out. reduce then starts from the first element, and an empty array has no first element, so reduce throws TypeError: Reduce of empty array with no initial value. A filter can give an empty array even when its input is not empty, so give reduce a start value.

main.ts
const votes = [120, 85, 240, 60];

let byLoop = 0;
for (const v of votes) {
  byLoop = byLoop + v;
}

const total = votes.reduce((sum, v) => sum + v, 0);
console.log(total); // 505

reduce combines all elements into one value, starting from the start value; with a start value, an empty array gives that value back.

Coming from Python: functools.reduce(f, items, start) is the same fold, and without start it fails on an empty list in the same way.

Precisely: choosing the start value

The start value is also the answer for an empty array, and it takes part in every result. For a sum it is 0, and for a product 1. For a largest value there is no ordinary number that fits, so decide first what an empty array should give, and handle that case before calling reduce. reduceRight works the same way, from the last element to the first.

Experiment

Add up a filtered array, then an empty one

reduce calls the callback once per element and passes the result so far and the element. The first call gets the start value. For an empty array there is no call, and reduce returns the start value. Without a start value, reduce starts from the first element, and an empty array has none.

Why it matters A filter can leave nothing even when its input is not empty. Give reduce a start value, so “nothing” gives that value instead of an error.

Predict total and noneTotal. Then delete , 0 from line 5, so that none.reduce gets no start value, and predict again.

const votes = [120, 85, 240, 60];
const finalists = votes.filter((v) => v >= 100);
const total = finalists.reduce((sum, v) => sum + v, 0);
const none = votes.filter((v) => v >= 300);
const noneTotal = none.reduce((sum, v) => sum + v, 0);
console.log(total, noneTotal);
What to look for

The filter on line 2 calls its arrow function four times, once per vote. On line 3 the reduce callback appears twice: first with sum 0 and v 120, then with sum 120 and v 240. total shows 360. On line 4, none shows [], and on line 5 no callback appears at all: noneTotal shows 0, the start value. The run prints 360 0. After the edit, the run stops at line 5 with TypeError: Reduce of empty array with no initial value, and nothing is printed.

Expected output

360 0

04 / Timing

When does the work happen?

In TypeScript, each array method does all its work as soon as it is called. votes.map(…) calls its callback for every element and returns a finished array; only then does .filter(…) start, on that new array. A chain of array methods works one whole array at a time, and makes a new array at each step.

This matters when a callback does something besides returning a value, such as printing a line: that happens at once, even when nothing uses the result. Rust iterators, Java streams, and Haskell lists wait until a result is needed; a TypeScript array does not.

main.ts
const votes = [120, 85, 240, 60];
// map calls its callback 4 times, now.
const withJudges = votes.map((v) => v + 10);
// filter calls its callback 4 more times, on the new array.
const finalists = withJudges.filter((v) => v >= 100);

Array methods run at once, one whole array at a time; each step makes a new array.

Coming from Java: A Java stream waits for a final operation and goes element by element; an array method runs at once and goes array by array.

Precisely: lazy steps in JavaScript

Iterator helpers, such as votes.values().map(…), which Node.js 22 supports, work one element at a time and only when a result is asked for, like Rust iterators. Generators work the same way. Array methods never do.

Experiment

See map finish before filter starts

Each array method does all its work as soon as it is called and returns a finished array. filter starts only after map has returned, so the work goes one whole array at a time.

Why it matters In TypeScript, a callback in map or filter runs at once, whether or not anything uses the result.

Predict the order of the add and check lines. Then delete the last line, console.log(finalists);, and predict which lines are still printed.

const votes = [120, 85, 240];
const withJudges = votes.map((v) => {
  console.log("add " + v);
  return v + 10;
});
const finalists = withJudges.filter((v) => {
  console.log("check " + v);
  return v >= 100;
});
console.log(finalists);
What to look for

Step through. The map callback runs for all three votes before the filter callback runs once: the output shows add 120, add 85, add 240, and only then check 130, check 95, check 250. When line 6 starts, withJudges already shows the whole new array [130, 95, 250]. The run then prints [ 130, 250 ]. After the edit, the six add and check lines are still printed.

Expected output

add 120
add 85
add 240
check 130
check 95
check 250
[ 130, 250 ]

05 / Order

Does it matter whether filter comes before or after map?

filter and map can be combined in either order, and the order can change the result. Put the test where the value it talks about exists. To keep the acts that reach 100 votes after the judges’ 10 are added, add first, then test. Testing first reads the old votes, so the act with 95 votes is dropped, although it ends with 105.

When both orders mean the same thing, filtering first calls map’s callback for fewer elements.

main.ts
const votes = [120, 95, 60];
const testFirst = votes
  .filter((v) => v >= 100)
  .map((v) => v + 10);
const addFirst = votes
  .map((v) => v + 10)
  .filter((v) => v >= 100);
console.log(testFirst); // [ 130 ]
console.log(addFirst); // [ 130, 105 ]

Put filter before map when the test reads the original value, and after map when it reads the new one.

06 / Try it

Can you explain a fresh example?

The judges give each act a score. Count the callback calls before you add anything up.

main.ts
const scores = [7, 3, 9, 4];
const doubled = scores
  .filter((s) => s > 4)
  .map((s) => s * 2);
const total = doubled.reduce((sum, s) => sum + s, 1);
console.log(doubled, total);

What is printed, and how many times does each callback run?

Reveal the reasoning

[ 14, 18 ] 33. filter’s callback runs four times, once per score, and keeps 7 and 9. map’s callback runs twice, on the kept scores, and gives 14 and 18. reduce’s callback runs twice: 1 + 14 is 15, then 15 + 18 is 33. The start value 1 takes part in the total, so it is not the same as a sum that starts at 0.

Practice tickets

  1. MFR-101 · The monthly bill fails for a band with only short bookings → Free
  2. MFR-102 · Some cheap student tickets are missing from the list → Sign in · Pro
  3. MFR-103 · Start times are wrong when pieces differ in length → Sign in · Pro

Keep exploring

Stack and heap →

Primary references