15 minlesson

Array Transformations Deep Dive

Array Transformations Deep Dive

This lesson explores advanced patterns for array transformation and data manipulation.

Understanding reduce()

reduce() is the most powerful array method - map and filter can be implemented with it:

javascript
1// reduce signature
2array.reduce((accumulator, currentValue, index, array) => {
3 // return new accumulator
4}, initialValue);
5
6// The accumulator "accumulates" results through each iteration

Building Common Operations with reduce

javascript
1// Implement map with reduce
2function map(arr, fn) {
3 return arr.reduce((acc, item, i) => {
4 acc.push(fn(item, i));
5 return acc;
6 }, []);
7}
8
9// Implement filter with reduce
10function filter(arr, predicate) {
11 return arr.reduce((acc, item) => {
12 if (predicate(item)) acc.push(item);
13 return acc;
14 }, []);
15}
16
17// Implement find with reduce
18function find(arr, predicate) {
19 return arr.reduce((found, item) => {
20 if (found !== undefined) return found;
21 return predicate(item) ? item : undefined;
22 }, undefined);
23}

Advanced reduce Patterns

Object Transformation

javascript
1// Array to object (by key)
2const users = [
3 { id: 1, name: "Alice" },
4 { id: 2, name: "Bob" }
5];
6
7const usersById = users.reduce((acc, user) => {
8 acc[user.id] = user;
9 return acc;
10}, {});
11// { 1: { id: 1, name: "Alice" }, 2: { id: 2, name: "Bob" } }
12
13// Alternative using Object.fromEntries
14const usersById2 = Object.fromEntries(
15 users.map(u => [u.id, u])
16);

Grouping and Categorizing

javascript
1const transactions = [
2 { type: "income", amount: 1000 },
3 { type: "expense", amount: 500 },
4 { type: "income", amount: 2000 },
5 { type: "expense", amount: 300 }
6];
7
8// Group by type
9const grouped = transactions.reduce((acc, t) => {
10 acc[t.type] = acc[t.type] || [];
11 acc[t.type].push(t);
12 return acc;
13}, {});
14
15// Sum by type
16const totals = transactions.reduce((acc, t) => {
17 acc[t.type] = (acc[t.type] || 0) + t.amount;
18 return acc;
19}, {});
20// { income: 3000, expense: 800 }
21
22// ES2024: Object.groupBy (coming soon to all browsers)
23const grouped2 = Object.groupBy(transactions, t => t.type);

Flattening with reduce

javascript
1// Flatten one level
2const flatten = arr => arr.reduce(
3 (flat, item) => flat.concat(item),
4 []
5);
6
7// Deep flatten
8const deepFlatten = arr => arr.reduce(
9 (flat, item) => flat.concat(
10 Array.isArray(item) ? deepFlatten(item) : item
11 ),
12 []
13);
14
15deepFlatten([1, [2, [3, [4]]]]); // [1, 2, 3, 4]

Running Calculations

javascript
1// Running total
2const amounts = [100, 50, 200, 75];
3const runningTotal = amounts.reduce((acc, amount) => {
4 const lastTotal = acc.length ? acc[acc.length - 1] : 0;
5 acc.push(lastTotal + amount);
6 return acc;
7}, []);
8// [100, 150, 350, 425]
9
10// Average with reduce
11const average = amounts.reduce((acc, val, i, arr) => {
12 acc += val;
13 if (i === arr.length - 1) return acc / arr.length;
14 return acc;
15}, 0);

Immutable Array Operations

Avoid mutating original arrays:

javascript
1// Immutable push
2const addItem = (arr, item) => [...arr, item];
3
4// Immutable unshift
5const prependItem = (arr, item) => [item, ...arr];
6
7// Immutable remove by index
8const removeAt = (arr, index) => [
9 ...arr.slice(0, index),
10 ...arr.slice(index + 1)
11];
12
13// Immutable update by index
14const updateAt = (arr, index, newValue) => [
15 ...arr.slice(0, index),
16 newValue,
17 ...arr.slice(index + 1)
18];
19
20// Immutable insert at index
21const insertAt = (arr, index, item) => [
22 ...arr.slice(0, index),
23 item,
24 ...arr.slice(index)
25];
26
27// Immutable sort
28const sortedCopy = arr => [...arr].sort();

Transforming Nested Data

javascript
1const data = {
2 users: [
3 { id: 1, orders: [{ total: 100 }, { total: 200 }] },
4 { id: 2, orders: [{ total: 150 }] }
5 ]
6};
7
8// Get all order totals (flatten nested)
9const allTotals = data.users.flatMap(user =>
10 user.orders.map(order => order.total)
11);
12// [100, 200, 150]
13
14// Transform nested structure
15const transformed = {
16 ...data,
17 users: data.users.map(user => ({
18 ...user,
19 totalSpent: user.orders.reduce((sum, o) => sum + o.total, 0)
20 }))
21};
22
23// Deep update (immutable)
24const updateUserOrder = (data, userId, orderIndex, newTotal) => ({
25 ...data,
26 users: data.users.map(user =>
27 user.id !== userId ? user : {
28 ...user,
29 orders: user.orders.map((order, i) =>
30 i !== orderIndex ? order : { ...order, total: newTotal }
31 )
32 }
33 )
34});

Pipeline Patterns

Creating reusable transformation pipelines:

javascript
1// Pipe function
2const pipe = (...fns) => (value) =>
3 fns.reduce((acc, fn) => fn(acc), value);
4
5// Reusable transformations
6const filterActive = users => users.filter(u => u.active);
7const sortByName = users => [...users].sort((a, b) =>
8 a.name.localeCompare(b.name)
9);
10const take = n => arr => arr.slice(0, n);
11const pluck = key => arr => arr.map(item => item[key]);
12
13// Compose pipeline
14const getTopActiveNames = pipe(
15 filterActive,
16 sortByName,
17 take(5),
18 pluck('name')
19);
20
21const users = [/* ... */];
22const result = getTopActiveNames(users);

Performance Considerations

javascript
1// BAD: Multiple iterations
2const result = arr
3 .filter(x => x > 0)
4 .map(x => x * 2)
5 .filter(x => x < 100);
6
7// BETTER: Single pass with reduce
8const result2 = arr.reduce((acc, x) => {
9 if (x > 0) {
10 const doubled = x * 2;
11 if (doubled < 100) {
12 acc.push(doubled);
13 }
14 }
15 return acc;
16}, []);
17
18// Even for small arrays, reduce is often cleaner when
19// you need multiple operations that depend on each other

Early Exit Patterns

javascript
1// some() and every() exit early
2arr.some(x => x > 10); // Stops at first true
3arr.every(x => x > 0); // Stops at first false
4
5// find() exits early
6arr.find(x => x > 10); // Stops at first match
7
8// for...of allows break
9function findFirst(arr, predicate) {
10 for (const item of arr) {
11 if (predicate(item)) return item;
12 }
13 return undefined;
14}
15
16// reduce() cannot exit early - consider for loop for large arrays
17// when you need early termination

Common Gotchas

javascript
1// 1. Forgetting initial value in reduce
2[].reduce((a, b) => a + b); // TypeError!
3[].reduce((a, b) => a + b, 0); // 0 (correct)
4
5// 2. Mutating in map/filter (don't do this!)
6const users = [{ active: false }];
7const activated = users.map(u => {
8 u.active = true; // BAD: mutates original!
9 return u;
10});
11// Instead:
12const activated2 = users.map(u => ({ ...u, active: true }));
13
14// 3. Using indexOf with objects
15[{ id: 1 }].indexOf({ id: 1 }); // -1 (different references)
16[{ id: 1 }].findIndex(x => x.id === 1); // 0 (correct)
17
18// 4. sort() mutates and has string default
19const nums = [10, 2, 1];
20const sorted = nums.sort(); // nums is also mutated!
21// Always: [...nums].sort((a, b) => a - b)
22
23// 5. flat() depth defaults to 1
24[[1], [[2]]].flat(); // [1, [2]] - only one level!

Utility Functions

Reusable array utilities:

javascript
1// Unique values
2const unique = arr => [...new Set(arr)];
3
4// Unique by key
5const uniqueBy = (arr, key) => {
6 const seen = new Set();
7 return arr.filter(item => {
8 const val = item[key];
9 if (seen.has(val)) return false;
10 seen.add(val);
11 return true;
12 });
13};
14
15// Intersection
16const intersection = (a, b) => a.filter(x => b.includes(x));
17
18// Difference
19const difference = (a, b) => a.filter(x => !b.includes(x));
20
21// Zip arrays
22const zip = (...arrays) => {
23 const maxLength = Math.max(...arrays.map(a => a.length));
24 return Array.from({ length: maxLength }, (_, i) =>
25 arrays.map(arr => arr[i])
26 );
27};
28
29zip([1, 2], ['a', 'b'], [true, false]);
30// [[1, 'a', true], [2, 'b', false]]
31
32// Shuffle
33const shuffle = arr => {
34 const copy = [...arr];
35 for (let i = copy.length - 1; i > 0; i--) {
36 const j = Math.floor(Math.random() * (i + 1));
37 [copy[i], copy[j]] = [copy[j], copy[i]];
38 }
39 return copy;
40};

Summary

PatternWhen to Use
reduce() to objectConverting array to lookup map
reduce() groupingCategorizing by property
flatMap()Map + flatten in one step
Immutable updatesWhen you shouldn't mutate
PipelinesReusable transformation chains
Early exit loopsLarge arrays with early termination

Key takeaways:

  • reduce() is powerful but often overkill - prefer simpler methods when possible
  • Always consider immutability
  • Chain methods for readable transformations
  • Be aware of performance for large arrays