Arrays

Transform a list without a loop, and know which methods change the original.

11of 32
≈ 13Min read
8Parts

The three that do most of the work

  • map always returns an array of the same length. If you are dropping items, you want filter.
  • filter keeps the items whose callback is truthy.
  • reduce folds a list into one value — a number, a string, an object, another array.
map, filter, reduce
const doors = [
  { name: "Painted", days: 5,  archived: false },
  { name: "Veneer",  days: 10, archived: false },
  { name: "Ceramic", days: 15, archived: true  },
];

doors.map((d) => d.name);                    // ["Painted","Veneer","Ceramic"]
doors.filter((d) => !d.archived);            // two objects
doors.reduce((sum, d) => sum + d.days, 0);   // 30

reduce, properly

four shapes you will actually write
// a total
doors.reduce((sum, d) => sum + d.days, 0);

// the best one — no initial value, so it starts from the first item
doors.reduce((best, d) => (d.days < best.days ? d : best));

// group into an object
doors.reduce((acc, d) => {
  const key = d.days < 10 ? "fast" : "slow";
  (acc[key] ??= []).push(d);
  return acc;
}, {});

// or just use the built-in
Object.groupBy(doors, (d) => (d.days < 10 ? "fast" : "slow"));
Always pass the initial value

Without it, reduce on an empty array throws. With it, an empty array simply gives you the initial value — which is almost always what you want.

Finding and testing

some on an empty array is false; every on an empty array is true. That second one surprises people, and it is mathematically correct: nothing failed.

all of these stop early
doors.find((d) => d.days > 10);        // the item, or undefined
doors.findIndex((d) => d.days > 10);   // the index, or -1
doors.findLast((d) => !d.archived);
doors.some((d) => d.archived);         // true if any
doors.every((d) => d.days > 0);        // true if all
doors.includes(x);                     // by identity — for primitives
doors.indexOf(x);

Changing versus returning

the one that bites
const sorted = doors.sort((a, b) => a.days - b.days);
// doors is ALSO sorted now — sort works in place and returns the same array

const sorted = [...doors].sort((a, b) => a.days - b.days);   // a copy
const sorted = doors.toSorted((a, b) => a.days - b.days);    // the modern way
Changes the originalReturns a new array
push pop shift unshiftconcat slice map filter flat
splice sort reverse filltoSorted toReversed toSpliced with
Sorting without a comparator sorts as strings

[10, 9, 1].sort() gives [1, 10, 9]. Always pass (a, b) => a - b for numbers and (a, b) => a.localeCompare(b) for text.

Spread and destructuring

A spread copy is shallow: the new array is new, the objects inside it are the same objects. Changing copy[0].name changes the original too.

copying and unpacking
const copy = [...list];              // a shallow copy
const joined = [...a, ...b];
const withExtra = [...list, item];   // instead of push, when you want a new array

const [first, second, ...rest] = list;
const [, , third] = list;            // skip with commas
const [x = 0] = [];                  // a default for a missing item

// swap
[a, b] = [b, a];

Building arrays

from nothing, or from something else
Array.from({ length: 5 }, (_, i) => i * 2);     // [0,2,4,6,8]
Array.from(document.querySelectorAll("li"));    // a real array from a NodeList
[...new Set(list)];                             // unique values
Array(3).fill(0);                               // [0,0,0]
"a,b,c".split(",");
Array(3) is not [undefined, undefined, undefined]

It is three holes, and map skips holes entirely. Array(3).map((_, i) => i) gives you three holes back. Use Array.from({ length: 3 }, …).

Flattening

flat and flatMap
[[1, 2], [3, [4]]].flat();        // [1, 2, 3, [4]]
[[1, 2], [3, [4]]].flat(2);       // [1, 2, 3, 4]
[[1, 2], [3]].flat(Infinity);

// map then flatten one level — good for "zero or more per item"
doors.flatMap((d) => (d.archived ? [] : [d.name]));

Chaining, and when to stop

Each step in a chain walks the whole array. For a few hundred items that is irrelevant. For a hundred thousand, one loop that does all three beats three passes — but measure before you make that trade, because the chain is far easier to read.

readable up to a point
const summary = doors
  .filter((d) => !d.archived)
  .sort((a, b) => a.days - b.days)
  .map((d) => `${d.name}: ${d.days}d`)
  .join(" · ");

Practice