Iterating Arrays: map, filter, reduce in Depth

โ–ถ Try It Yourself

You met map, filter, and reduce as higher-order functions in Chapter 3. In this lesson we go deeper โ€” focusing specifically on their array-centric patterns, combining them into data pipelines, and avoiding the performance and mutation pitfalls that trip up even experienced developers. You will also learn flatMap, Array.from with a mapping function, and practical patterns for transforming real-world data shapes.

map vs filter vs reduce โ€” Decision Guide

Question Method Output Shape
“Transform every element into something else” map Same-length array
“Keep only elements that pass a test” filter Shorter or equal array
“Combine all elements into one value/object” reduce Any single value
“Transform then flatten one level” flatMap Flattened array
“Run a side-effect per element” forEach undefined
“Transform an iterable (not an array)” Array.from(iter, fn) New array

reduce Accumulator Cheat Sheet

Task Initial Value Return from Callback
Sum 0 acc + curr.value
Max value -Infinity curr > acc ? curr : acc
Count occurrences {} { ...acc, [curr]: (acc[curr] ?? 0) + 1 }
Group by key {} Push to acc[key] array
Index by id {} { ...acc, [curr.id]: curr }
Flatten [] [...acc, ...curr]
Partition (split) [[], []] Push curr into acc[0] or acc[1]

flatMap vs map + flat

Method Effect Performance
arr.map(fn).flat() Map then flatten one level Two passes over data
arr.flatMap(fn) Map and flatten in one pass More efficient โ€” single pass
arr.flatMap(fn) returning [] Effectively filters the element out Combined filter + map
Note: reduce without an initial value uses the first element as the accumulator and starts iterating from the second element. This can cause subtle bugs: on an empty array it throws a TypeError, and on a single-element array it returns that element without ever calling the callback. Always provide an initial value unless you have a very specific reason not to.
Tip: flatMap is perfect for a combined filter-and-map in a single pass. If the callback returns an empty array [] for elements you want to skip and a one-element array [transformed] for elements to keep, flatMap gives you the filtered, transformed result: users.flatMap(u => u.active ? [u.name.toUpperCase()] : []).
Warning: Avoid mutating the original array or external state inside map, filter, or reduce callbacks. These methods are designed for pure transformations โ€” predictable, side-effect-free operations that produce the same output for the same input. Mutation inside callbacks breaks this guarantee and leads to hard-to-trace bugs, especially in React and other reactive frameworks.

Basic Example

const orders = [
    { id: 'O1', customer: 'Alice', status: 'completed', items: [{ name: 'Widget', qty: 2, price: 9.99 }, { name: 'Bolt', qty: 10, price: 0.50 }] },
    { id: 'O2', customer: 'Bob',   status: 'pending',   items: [{ name: 'Gadget', qty: 1, price: 49.00 }] },
    { id: 'O3', customer: 'Carol', status: 'completed', items: [{ name: 'Widget', qty: 1, price: 9.99 }, { name: 'Wrench', qty: 2, price: 12.50 }] },
    { id: 'O4', customer: 'Dave',  status: 'cancelled', items: [{ name: 'Bolt', qty: 5, price: 0.50 }] },
];

// โ”€โ”€ map โ€” add computed total to each order โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const withTotals = orders.map(order => ({
    ...order,
    total: order.items.reduce((sum, item) => sum + item.qty * item.price, 0),
}));
console.log(withTotals.map(o => `${o.id}: $${o.total.toFixed(2)}`));
// ['O1: $25.00', 'O2: $49.00', 'O3: $34.99', 'O4: $2.50']

// โ”€โ”€ filter โ€” completed orders only โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const completed = withTotals.filter(o => o.status === 'completed');
console.log(completed.map(o => o.customer));  // ['Alice', 'Carol']

// โ”€โ”€ reduce โ€” total revenue from completed orders โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const revenue = completed.reduce((sum, o) => sum + o.total, 0);
console.log(`Revenue: $${revenue.toFixed(2)}`);   // Revenue: $59.99

// โ”€โ”€ reduce โ€” group orders by status โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const byStatus = orders.reduce((groups, order) => {
    const key = order.status;
    groups[key] = groups[key] ?? [];
    groups[key].push(order.id);
    return groups;
}, {});
console.log(byStatus);
// { completed: ['O1','O3'], pending: ['O2'], cancelled: ['O4'] }

// โ”€โ”€ reduce โ€” build lookup index โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const orderIndex = orders.reduce((idx, order) => ({ ...idx, [order.id]: order }), {});
console.log(orderIndex['O2'].customer);   // 'Bob'

// โ”€โ”€ flatMap โ€” expand items from all completed orders โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const allItems = completed.flatMap(o => o.items);
console.log(allItems.map(i => i.name));
// ['Widget', 'Bolt', 'Widget', 'Wrench']

// โ”€โ”€ flatMap as filter + map โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const expensiveItemNames = orders.flatMap(o =>
    o.items
        .filter(i => i.price >= 5)
        .map(i => `${o.customer}: ${i.name} $${i.price}`)
);
console.log(expensiveItemNames);
// ['Alice: Widget $9.99', 'Bob: Gadget $49.00', 'Carol: Widget $9.99', 'Carol: Wrench $12.50']

// โ”€โ”€ reduce โ€” partition into two groups โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const [active, inactive] = orders.reduce(
    ([on, off], o) => o.status === 'completed'
        ? [[...on, o.id], off]
        : [on, [...off, o.id]],
    [[], []]
);
console.log('Active:', active);    // ['O1', 'O3']
console.log('Inactive:', inactive); // ['O2', 'O4']

// โ”€โ”€ Chained pipeline โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const topCustomers = orders
    .filter(o => o.status === 'completed')
    .map(o => ({ ...o, total: o.items.reduce((s, i) => s + i.qty * i.price, 0) }))
    .sort((a, b) => b.total - a.total)
    .map(o => `${o.customer}: $${o.total.toFixed(2)}`);

console.log(topCustomers);
// ['Carol: $34.99', 'Alice: $25.00']

How It Works

Step 1 โ€” Spread + map Produces New Objects

orders.map(o => ({ ...o, total: ... })) spreads each order into a new object and adds the computed total property. This keeps the original orders array completely untouched โ€” each mapped element is a new object, not a reference to the original.

Step 2 โ€” Chaining Builds Data Pipelines

Each method in a chain receives the output of the previous method. filter narrows the array, map transforms it, sort orders it, and a final map formats the output for display. This declarative pipeline style expresses what to do, not how โ€” making it easy to read, modify, and test.

Step 3 โ€” reduce Can Replace Any Iteration Pattern

The grouping and indexing patterns show that reduce is fundamentally a general-purpose fold operation โ€” it can produce any output shape. When the accumulator is an object, you can build any data structure from an array in a single pass. The key is to always return the accumulator from the callback.

Step 4 โ€” flatMap Expands One-to-Many Relationships

Each order has multiple items. flatMap(o => o.items) maps each order to its items array, then flattens one level โ€” giving a single flat array of all items across all orders. This is the canonical pattern for extracting nested collections.

Step 5 โ€” Partition with reduce([[], []])

The partition pattern uses a [[], []] initial accumulator โ€” an array of two arrays. The callback pushes each element into the first or second bucket based on a condition. Destructuring the result gives two named arrays: const [passing, failing] = arr.reduce(partitionFn, [[], []]).

Real-World Example: Analytics Dashboard

// analytics.js

function buildDashboard(events) {
    return events.reduce((dash, event) => {
        // Count events by type
        dash.counts[event.type] = (dash.counts[event.type] ?? 0) + 1;

        // Track unique users
        dash.uniqueUsers.add(event.userId);

        // Sum revenue from purchase events
        if (event.type === 'purchase') {
            dash.revenue += event.value ?? 0;
        }

        // Collect error messages
        if (event.type === 'error') {
            dash.errors.push({ msg: event.message, time: event.ts });
        }

        return dash;
    }, {
        counts:      {},
        uniqueUsers: new Set(),
        revenue:     0,
        errors:      [],
    });
}

const events = [
    { type: 'pageview',  userId: 'u1', ts: 1700000000 },
    { type: 'purchase',  userId: 'u2', ts: 1700000010, value: 49.99 },
    { type: 'pageview',  userId: 'u2', ts: 1700000020 },
    { type: 'error',     userId: 'u3', ts: 1700000030, message: 'Payment failed' },
    { type: 'purchase',  userId: 'u1', ts: 1700000040, value: 29.99 },
    { type: 'pageview',  userId: 'u3', ts: 1700000050 },
];

const dashboard = buildDashboard(events);
console.log('Event counts:', dashboard.counts);
// { pageview: 3, purchase: 2, error: 1 }
console.log('Unique users:', dashboard.uniqueUsers.size);  // 3
console.log('Revenue: $' + dashboard.revenue.toFixed(2));   // $79.98
console.log('Errors:', dashboard.errors);

Common Mistakes

Mistake 1 โ€” Mutating inside map callback

โŒ Wrong โ€” modifying original objects inside map:

const result = users.map(u => {
    u.fullName = `${u.first} ${u.last}`;   // mutates original!
    return u;
});

โœ… Correct โ€” always return new objects from map:

const result = users.map(u => ({ ...u, fullName: `${u.first} ${u.last}` }));

Mistake 2 โ€” Nesting map+filter instead of flatMap

โŒ Awkward โ€” produces array of arrays:

const nested = orders.map(o => o.items.filter(i => i.price > 10));
// [[item1, item2], [], [item3]] โ€” needs .flat() afterwards

โœ… Correct โ€” flatMap does both in one pass:

const flat = orders.flatMap(o => o.items.filter(i => i.price > 10));
// [item1, item2, item3] โ€” already flat

Mistake 3 โ€” Forgetting to return accumulator from reduce

โŒ Wrong โ€” accumulator is undefined on next iteration:

const grouped = items.reduce((acc, item) => {
    acc[item.type] = acc[item.type] ?? [];
    acc[item.type].push(item);
    // forgot: return acc!
}, {});
// TypeError: Cannot set properties of undefined

โœ… Correct โ€” always return the accumulator:

const grouped = items.reduce((acc, item) => {
    acc[item.type] = acc[item.type] ?? [];
    acc[item.type].push(item);
    return acc;   // critical!
}, {});

▶ Try It Yourself

Quick Reference

Task One-liner
Sum a field arr.reduce((s,x) => s + x.val, 0)
Group by key arr.reduce((g,x) => { (g[x.key]??=[]).push(x); return g; }, {})
Index by id arr.reduce((m,x) => ({...m,[x.id]:x}), {})
Partition arr.reduce(([y,n],x) => fn(x)?[[...y,x],n]:[y,[...n,x]], [[],[]])
Flat expand arr.flatMap(x => x.children)
Filter + map arr.flatMap(x => cond(x) ? [transform(x)] : [])
Unique values [...new Set(arr.map(x => x.field))]

🧠 Test Yourself

Which method is most efficient for both transforming elements AND filtering out some in a single pass?





โ–ถ Try It Yourself