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 |
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.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()] : []).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!
}, {});
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))] |