JavaScript arrays ship with a rich set of built-in methods. Knowing which method to reach for — and how each one behaves at the edges — separates junior code from production-ready code.
Quick-reference table
| Method | Returns | Mutates? | Purpose |
|---|---|---|---|
map |
new array (same length) | No | Transform every element |
filter |
new array (≤ length) | No | Keep elements that pass a test |
reduce |
single value | No | Accumulate elements into one value |
forEach |
undefined |
No | Side effects per element |
find |
element | undefined |
No | First element matching predicate |
findIndex |
index | -1 |
No | Index of first match |
findLast |
element | undefined |
No | Last element matching predicate |
some |
boolean | No | True if any element passes |
every |
boolean | No | True if all elements pass |
includes |
boolean | No | True if value exists (uses ===) |
indexOf |
index | -1 |
No | First index of a value |
flat |
new array | No | Flatten nested arrays |
flatMap |
new array | No | map then flat(1) in one pass |
sort |
same array | Yes | Sort in place |
reverse |
same array | Yes | Reverse in place |
splice |
removed elements | Yes | Add/remove at index |
push / pop |
new length / removed | Yes | Add/remove at end |
shift / unshift |
removed / new length | Yes | Add/remove at start |
slice |
new array | No | Copy a range |
concat |
new array | No | Merge arrays |
join |
string | No | Elements to string |
fill |
same array | Yes | Fill range with value |
copyWithin |
same array | Yes | Copy segment within array |
at |
element | undefined |
No | Element at index (negatives work) |
Array.from |
new array | — | Create array from iterable |
Array.of |
new array | — | Create array from arguments |
Array.isArray |
boolean | — | Type check |
Transforming: map
Creates a new array by applying a function to each element.
const prices = [10, 20, 30];
const withTax = prices.map(p => p * 1.2);
// [12, 24, 36] — prices unchanged
// TypeScript: map infers the return type
const lengths: number[] = ["hello", "world"].map(s => s.length);
// [5, 5]
Key rules:
- Always returns an array of the same length.
- Use
mapwhen you need a new array; useforEachwhen you only want side effects. - Never
pushinside amapcallback — that's aforEach.
Filtering: filter
Returns a new array containing only elements where the callback returns truthy.
const scores = [42, 95, 60, 78, 55];
const passing = scores.filter(s => s >= 60);
// [95, 60, 78]
Chain with map:
const users = [
{ name: "Alice", active: true },
{ name: "Bob", active: false },
{ name: "Carol", active: true },
];
const activeNames = users
.filter(u => u.active)
.map(u => u.name);
// ["Alice", "Carol"]
Accumulating: reduce
Reduces an array to a single value by running a callback on each element with an accumulator.
// Syntax: array.reduce(callback, initialValue)
const nums = [1, 2, 3, 4, 5];
const sum = nums.reduce((acc, n) => acc + n, 0);
// 15
Common reduce patterns
Sum / product:
const total = cart.reduce((sum, item) => sum + item.price, 0);
Count occurrences:
const votes = ["yes", "no", "yes", "yes", "no"];
const tally = votes.reduce((acc, v) => {
acc[v] = (acc[v] ?? 0) + 1;
return acc;
}, {});
// { yes: 3, no: 2 }
Group by a key:
const people = [
{ name: "Alice", dept: "Eng" },
{ name: "Bob", dept: "HR" },
{ name: "Carol", dept: "Eng" },
];
const byDept = people.reduce((groups, person) => {
const key = person.dept;
(groups[key] ??= []).push(person);
return groups;
}, {});
// { Eng: [{Alice}, {Carol}], HR: [{Bob}] }
Flatten one level (educational — use flat() in practice):
const nested = [[1, 2], [3, 4], [5]];
const flat = nested.reduce((acc, arr) => acc.concat(arr), []);
// [1, 2, 3, 4, 5]
Always provide an initial value. Calling
reduceon an empty array without one throws aTypeError.
Searching: find, findIndex, findLast
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Carol" },
];
// find — returns the element (or undefined)
const bob = users.find(u => u.id === 2);
// { id: 2, name: "Bob" }
// findIndex — returns the index (or -1)
const idx = users.findIndex(u => u.id === 2);
// 1
// findLast — searches from the end (ES2023)
const lastEven = [1, 2, 3, 4, 5].findLast(n => n % 2 === 0);
// 4
Use find / findLast when you want the element; use findIndex / findLastIndex when you need the position.
Boolean checks: some, every, includes
const ages = [12, 18, 25, 30];
ages.some(a => a >= 18); // true — at least one adult
ages.every(a => a >= 18); // false — not all adults
ages.includes(25); // true — 25 exists (uses ===)
Short-circuit behaviour:
somestops at the first truthy result.everystops at the first falsy result.
Flattening: flat, flatMap
// flat — default depth is 1
const nested = [1, [2, 3], [4, [5, 6]]];
nested.flat(); // [1, 2, 3, 4, [5, 6]]
nested.flat(2); // [1, 2, 3, 4, 5, 6]
nested.flat(Infinity); // fully flatten, any depth
// flatMap — map then flat(1) in one pass (more efficient)
const sentences = ["hello world", "foo bar"];
const words = sentences.flatMap(s => s.split(" "));
// ["hello", "world", "foo", "bar"]
flatMap is the idiomatic way to produce zero-or-more elements per input element:
// Expand each order into its line items
const lineItems = orders.flatMap(o => o.items);
Sorting: sort
sort mutates the original array and converts elements to strings by default.
// Wrong — lexicographic order
[10, 1, 20, 2].sort(); // [1, 10, 2, 20] ⚠️
// Correct — numeric comparator
[10, 1, 20, 2].sort((a, b) => a - b); // [1, 2, 10, 20]
[10, 1, 20, 2].sort((a, b) => b - a); // [20, 10, 2, 1] (descending)
Sort strings with localeCompare:
["banana", "Apple", "cherry"].sort((a, b) =>
a.localeCompare(b, undefined, { sensitivity: "base" })
);
// ["Apple", "banana", "cherry"]
Sort objects:
users.sort((a, b) => a.name.localeCompare(b.name));
Immutable sort (ES2023 toSorted):
const sorted = arr.toSorted((a, b) => a - b); // original untouched
Slicing and splicing
const arr = [1, 2, 3, 4, 5];
// slice — non-mutating, copies a range
arr.slice(1, 3); // [2, 3] (index 1 up to, not including, 3)
arr.slice(-2); // [4, 5] (last 2)
// splice — mutating: remove / insert
arr.splice(2, 1); // removes 1 element at index 2 → arr is now [1,2,4,5]
arr.splice(2, 0, 99); // inserts 99 at index 2 → [1,2,99,4,5]
Immutable alternatives (ES2023):
arr.toSpliced(2, 1); // like splice but returns new array
arr.with(2, 99); // replace element at index 2, returns new array
arr.toReversed(); // like reverse but non-mutating
Iterating: forEach
users.forEach(user => {
console.log(user.name);
});
Rules:
- Returns
undefined— you cannot chain.mapafterforEach. - Cannot be stopped early (use
for...ofwithbreakif you need that). - Do not use for transformations — use
map/filterinstead.
Utility methods
// at — supports negative indices (ES2022)
const last = [1, 2, 3].at(-1); // 3
// join — array to string
["a", "b", "c"].join(", "); // "a, b, c"
["a", "b", "c"].join(""); // "abc"
// concat — merge without mutation
[1, 2].concat([3, 4], [5]); // [1, 2, 3, 4, 5]
// fill — fill a range (mutating)
new Array(5).fill(0); // [0, 0, 0, 0, 0]
[1, 2, 3, 4].fill(0, 1, 3); // [1, 0, 0, 4]
// reverse — in place (mutating)
[1, 2, 3].reverse(); // [3, 2, 1]
Creating arrays: Array.from, Array.of, spread
// Array.from — from iterable or array-like
Array.from("hello"); // ["h","e","l","l","o"]
Array.from({ length: 5 }, (_, i) => i * 2); // [0,2,4,6,8]
Array.from(new Set([1, 2, 2, 3])); // [1, 2, 3]
// Array.of — create array from arguments (unlike Array(3) which makes 3 empty slots)
Array.of(3); // [3] — one element
Array(3); // [,,] — 3 empty slots ⚠️
// Spread — convert or clone
const clone = [...original];
const merged = [...arr1, ...arr2];
TypeScript typed array methods
interface User {
id: number;
name: string;
active: boolean;
}
const users: User[] = [
{ id: 1, name: "Alice", active: true },
{ id: 2, name: "Bob", active: false },
];
// map infers return type
const names: string[] = users.map(u => u.name);
// filter with type predicate
const active: User[] = users.filter((u): u is User => u.active);
// reduce with explicit accumulator type
const nameMap: Record<number, string> = users.reduce<Record<number, string>>(
(acc, u) => ({ ...acc, [u.id]: u.name }),
{}
);
// find returns User | undefined
const found: User | undefined = users.find(u => u.id === 1);
Chaining methods
const result = products
.filter(p => p.inStock) // keep in-stock
.map(p => ({ ...p, price: p.price * 0.9 })) // 10% discount
.sort((a, b) => a.price - b.price) // cheapest first
.slice(0, 5); // top 5
Each method (except
sortandreverse) returns a new array, making safe chaining possible. For large datasets, consider a singlereducepass or a library like Lodash to avoid creating multiple intermediate arrays.
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
[10,1,2].sort() |
Lexicographic, not numeric | Pass comparator: .sort((a,b) => a-b) |
arr.reduce((acc,x) => acc + x) on empty array |
TypeError: Reduce of empty array with no initial value |
Always provide initial value: reduce(fn, 0) |
Using map for side effects only |
Returns unused array; linter warns | Use forEach for side effects |
Mutating sort on a state variable |
Triggers unexpected re-renders / bugs | Use [...arr].sort(fn) or arr.toSorted(fn) |
forEach with async callback |
Does not await promises | Use for...of with await or Promise.all(arr.map(...)) |
arr.find(x => x === val) |
Returns undefined silently for primitives |
Use arr.includes(val) for existence checks |
splice when you want slice |
Mutates original | Use slice for non-mutating copy |
Async patterns
// Wrong — forEach does not await
await arr.forEach(async item => {
await fetch(item.url); // fires but forEach doesn't wait
});
// Correct — sequential
for (const item of arr) {
await fetch(item.url);
}
// Correct — parallel
const results = await Promise.all(arr.map(item => fetch(item.url)));
// Correct — parallel with concurrency limit (batch of 3)
async function batchProcess(arr, fn, batchSize = 3) {
const results = [];
for (let i = 0; i < arr.length; i += batchSize) {
const batch = arr.slice(i, i + batchSize);
results.push(...await Promise.all(batch.map(fn)));
}
return results;
}
Performance notes
- Avoid
forEach+pushinside a loop whenmapdoes the same job — same performance, clearer intent. - Chain wisely:
.filter().map()creates two arrays. For large arrays, a single.reduce()pass avoids the intermediate allocation. sortis O(n log n); for already-nearly-sorted data it's fast in practice (V8 uses Timsort).flat(Infinity)on deeply nested arrays can be slow — prefer iterative or recursive approaches if depth is unknown and data is huge.includesvsSet.has:includesis O(n); if you check membership repeatedly, convert to aSetfirst.
// Repeated includes — O(n²) overall
items.filter(x => bigList.includes(x));
// Better — O(n) overall
const set = new Set(bigList);
items.filter(x => set.has(x));
FAQ
Q: When should I use reduce vs map + filter?
Use reduce when you're producing a non-array result (object, number, string) or need a single pass over the data. Use map + filter chains when readability matters more than micro-performance — they're self-documenting.
Q: Does map skip holes in sparse arrays?
Yes — map, filter, forEach, find, and reduce all skip holes (empty slots). for...of and Array.from do not. This is rarely important in practice; avoid sparse arrays.
Q: What's the difference between find and filter?find returns the first matching element (or undefined). filter returns all matching elements as a new array. Use find when you expect at most one match.
Q: Is it safe to chain sort directly?
No — sort mutates the original. Either copy first ([...arr].sort(fn)) or use the ES2023 toSorted method.
Q: Can I break out of forEach early?
No. Use for...of with break, or use some / every whose short-circuit behaviour can act as an early exit.
Q: What does flatMap do that map + flat doesn't?
Functionally they're equivalent (flatMap always flattens one level). flatMap is slightly more efficient because it combines both passes internally — worth preferring when both operations are needed.