ESC

Type to search the knowledge base.

Array map, filter, reduce

map, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.

beginner4 min read
  • javascript
  • arrays
  • map
  • filter
  • reduce

Frontend code is mostly “take a list, produce another list or a single value.” map, filter, and reduce name those three jobs. Interviewers care less about the API and more about whether you mutate, whether you chain wastefully, and whether you can implement reduce from scratch.

Three jobs

const nums = [1, 2, 3, 4, 5];

nums.map((n) => n * 2);        // [2, 4, 6, 8, 10]  — same length, project
nums.filter((n) => n % 2 === 0); // [2, 4]          — subset
nums.reduce((sum, n) => sum + n, 0); // 15          — aggregate
  • map — one output element per input (1:1 shape).
  • filter — keep elements where the predicate is truthy.
  • reduce — fold the list into any single value (number, object, Map, array…).

None of them mutate the original array (unless you mutate objects inside). The array reference is new for map/filter; reduce returns whatever you build.

map and filter in UI code

const visible = todos
  .filter((t) => (showDone ? true : !t.done))
  .map((t) => ({ ...t, label: t.title.toUpperCase() }));

Prefer pure callbacks. Side effects (DOM writes, network) inside map make order and re-runs hard to reason about — put those in forEach or a loop if you must, or better, outside.

reduce: start with an initial value

// group by key
function groupBy(arr, keyFn) {
  return arr.reduce((acc, item) => {
    const k = keyFn(item);
    (acc[k] ??= []).push(item);
    return acc;
  }, {});
}

groupBy(
  [
    { type: 'fruit', name: 'apple' },
    { type: 'veg', name: 'carrot' },
    { type: 'fruit', name: 'pear' },
  ],
  (x) => x.type,
);
// { fruit: [...], veg: [...] }

Without an initial value, reduce uses the first element as the accumulator and starts at index 1 — easy to break on empty arrays:

[].reduce((a, b) => a + b); // TypeError
[].reduce((a, b) => a + b, 0); // 0

Always pass an initial value unless you deliberately want “first element” semantics.

Chaining cost

// two intermediate arrays
arr.filter(pred).map(fn);

// one pass
arr.reduce((acc, x) => {
  if (pred(x)) acc.push(fn(x));
  return acc;
}, []);

For small UI lists, chain for readability. For hot paths over huge arrays, one loop (or reduce) wins. Don’t optimize 20-item todo lists.

map is not for side effects

// bad smell
ids.map((id) => fetch(`/api/${id}`)); // returns promises you ignored

// intentional
await Promise.all(ids.map((id) => fetch(`/api/${id}`).then((r) => r.json())));

Implementing the mental model

function map(arr, fn) {
  const out = [];
  for (let i = 0; i < arr.length; i++) out.push(fn(arr[i], i, arr));
  return out;
}

function filter(arr, fn) {
  const out = [];
  for (let i = 0; i < arr.length; i++) {
    if (fn(arr[i], i, arr)) out.push(arr[i]);
  }
  return out;
}

function reduce(arr, fn, init) {
  let acc = init;
  let i = 0;
  if (arguments.length < 3) {
    if (arr.length === 0) throw new TypeError('Reduce of empty array');
    acc = arr[0];
    i = 1;
  }
  for (; i < arr.length; i++) acc = fn(acc, arr[i], i, arr);
  return acc;
}

If you can write that on a whiteboard, the built-ins stop being magic.

Footguns

Issue Detail
Sparse arrays Callbacks skip holes; length still includes them in some senses
Mutating objects inside map New array, same nested refs — shallow
filter(Boolean) Drops 0, '', false — may be intended
reduce object spreads in a loop acc = { ...acc, [k]: v } is O(n²) for large n

Interview answer

“map projects 1:1, filter selects, reduce folds to any value. I always give reduce an initial value. I chain filter/map for clarity on small data and collapse to one pass when profiling says so. I don’t use map for pure side effects.”

Further reading

Related guides