JavaScript arrays are the backbone of data manipulation in modern web development. With the ES2023 specification introducing immutable alternatives to long-standing mutating methods, now is the perfect time to revisit how we work with arrays. This article covers the full landscape of array methods, from the familiar map, filter, and reduce to cutting-edge additions, with practical examples and performance insights.
The Array Method Landscape
Array methods fall into two broad categories: mutating and non-mutating. Methods like push, pop, splice, sort, and reverse modify the array in place, while map, filter, slice, and the ES2023 additions create new arrays. The industry trend is shifting toward immutability, which reduces bugs caused by unintended side effects.
ES2023 introduced four immutable methods that address common pain points:
const arr = [3, 1, 4, 1, 5, 9];
const sorted = arr.toSorted(); // [1, 1, 3, 4, 5, 9] — new array
const reversed = arr.toReversed(); // [9, 5, 1, 4, 1, 3] — new array
const spliced = arr.toSpliced(1, 2); // [3, 1, 5, 9] — new array
const updated = arr.with(2, 42); // [3, 1, 42, 1, 5, 9] — new array
console.log(arr); // [3, 1, 4, 1, 5, 9] — unmodified
These methods make it safer to work with arrays in state management contexts like React or Redux, where immutability is a core principle.
Map, Filter, and Reduce in Depth
The triumvirate of functional array methods — map, filter, and reduce — enables expressive data transformations without explicit loops.
map transforms every element via a callback function, producing a new array of the same length:
const numbers = [1, 2, 3, 4, 5];
const squared = numbers.map(n => n * n);
// [1, 4, 9, 16, 25]
filter selects elements that pass a predicate test:
const even = numbers.filter(n => n % 2 === 0);
// [2, 4]
reduce is the most versatile — it can implement any of the other methods:
const sum = numbers.reduce((acc, n) => acc + n, 0);
// 15
// map implemented with reduce
const squared2 = numbers.reduce((acc, n) => [...acc, n * n], []);
A common anti-pattern is chaining map followed by filter when a single flatMap would suffice. Each chained method creates an intermediate array, which matters for large datasets.
| Method | Returns | Mutates Original | Use Case |
|---|---|---|---|
map | New array | No | Transform every element |
filter | New array | No | Select subset of elements |
reduce | Any value | No | Aggregate to single value |
sort | Same array | Yes | In-place ordering |
toSorted | New array | No | Immutable ordering |
flatMap and groupBy
flatMap combines mapping and flattening into a single pass. It is particularly useful when each input element maps to zero, one, or multiple output elements:
const sentences = ["hello world", "foo bar"];
const words = sentences.flatMap(s => s.split(" "));
// ["hello", "world", "foo", "bar"]
Object.groupBy (ES2024) provides native grouping support:
const users = [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "user" },
{ name: "Charlie", role: "user" },
];
const grouped = Object.groupBy(users, user => user.role);
// { admin: [{ name: "Alice", role: "admin" }],
// user: [{ name: "Bob", ... }, { name: "Charlie", ... }] }
This replaces the manual reduce-based grouping pattern that was previously necessary.
Functional Chaining Patterns
Functional chaining composes operations in readable pipelines:
const result = data
.filter(item => item.isActive)
.map(item => ({ ...item, score: item.value * 2 }))
.filter(item => item.score > 10)
.reduce((acc, item) => acc + item.score, 0);
Each method in the chain creates an intermediate array. For arrays under 10,000 elements this overhead is negligible. Beyond that, consider a single reduce pass or transducer libraries like Ramda to eliminate intermediate allocations.
Performance Considerations
Loop performance for simple operations generally follows this order (fastest first): for loop, for...of, forEach, map, filter, reduce. However, readability should be the default choice — only optimize when profiling reveals a bottleneck.
The find method benefits from early termination and can be orders of magnitude faster than filter followed by index access:
const found = arr.find(x => x.id === targetId); // O(n) average, stops early
const found2 = arr.filter(x => x.id === targetId)[0]; // O(n) always, full traversal
Real-World Recipes
Removing duplicates is elegantly handled with Set:
const unique = [...new Set([1, 2, 2, 3, 3, 4])];
// [1, 2, 3, 4]
Chunking an array into groups:
const chunk = (arr, size) =>
Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
arr.slice(i * size, i * size + size)
);
The Fisher-Yates shuffle provides unbiased randomization:
const shuffle = arr => {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
};
TypeScript Integration
Proper typing enhances array method usage. Type predicates in filter enable type narrowing:
const items: (string | null)[] = ["a", null, "b", null];
const strings: string[] = items.filter((x): x is string => x !== null);
For complex reductions, provide explicit accumulator types:
const grouped = items.reduce<Record<string, number[]>>((acc, item) => {
(acc[item.key] ??= []).push(item.value);
return acc;
}, {});
Immutability Patterns
The ES2023 immutable methods (toSorted, toReversed, toSpliced, with) should become your default for non-performance-critical code. They eliminate entire categories of mutation-related bugs while maintaining readability. In state management, combine them with the spread operator for complex updates:
const state = { items: [1, 2, 3], selected: null };
const next = {
...state,
items: state.items.with(0, 99).toSorted(),
};
Mastering array methods is essential for writing clean, efficient JavaScript. Start with map, filter, and reduce for everyday tasks, adopt flatMap and groupBy for specific patterns, and default to the ES2023 immutable methods whenever possible.
