A for loop can do anything, which is exactly the problem — the reader has to read the whole loop body to figure out what it's doing. Array methods name the operation up front.
Transforming: map
const prices = [10, 20, 30];
const withTax = prices.map((p) => p * 1.08);Reads as "transform each price" — no need to track an index or an accumulator array.
Selecting: filter
const users = [{ active: true }, { active: false }, { active: true }];
const activeUsers = users.filter((u) => u.active);Combining into one value: reduce
const cart = [{ price: 10 }, { price: 25 }, { price: 15 }];
const total = cart.reduce((sum, item) => sum + item.price, 0);reduce is the one people avoid, usually because the signature is unintuitive at first. The mental model: the second argument (0 here) is the starting value, and the callback runs once per item, returning the "running total" each time.
Finding one item: find
const user = users.find((u) => u.id === targetId);
// undefined if not found — no need to pre-check with a loopChecking conditions: some and every
const hasActiveUser = users.some((u) => u.active); // true if ANY match
const allActive = users.every((u) => u.active); // true if ALL matchBoth short-circuit — some stops at the first match, every stops at the first non-match — so they're not slower than a hand-written loop with an early break.
| Method | Returns | Use it to |
|---|---|---|
map | A new array, same length | Transform each item |
filter | A new array, subset | Select matching items |
reduce | A single accumulated value | Combine everything into one result |
find | The first match, or undefined | Locate one item |
some / every | true/false | Check a condition across the array |
When a plain loop is still the right call
- You need to
breakorcontinuebased on complex conditions mid-iteration (array methods don't support this cleanly). - You're mutating the array you're iterating over (risky with array methods, risky in general).
- Performance-critical hot paths processing very large arrays — a
forloop avoids the per-callback function-call overhead, though this rarely matters outside of tight inner loops in performance-sensitive code.
Chaining — where it helps and where it hurts
const total = cart
.filter((item) => item.inStock)
.map((item) => item.price * item.quantity)
.reduce((sum, price) => sum + price, 0);This reads left-to-right as a pipeline: filter, then transform, then combine. That's a genuine readability win over the equivalent loop. But chaining four or five methods on a large array means four or five full passes over the data — if performance matters and the array is large, a single reduce (or a plain loop) that does the filtering and transforming in one pass is worth the readability tradeoff.
Adding real types to these methods (the callback's parameter and return type both get inferred automatically in TypeScript) is one more reason to reach for them over a hand-rolled loop — see the TypeScript utility types cheat sheet for how far that type inference extends once you're working with Array<T> generically.
flatMap: mapping and flattening in one pass
flatMap is the method people know least, and it solves a specific, common problem — mapping each item to an array, then flattening the result by one level:
const orders = [
{ id: 1, items: ["shirt", "hat"] },
{ id: 2, items: ["socks"] },
];
const allItems = orders.flatMap((order) => order.items);
// ["shirt", "hat", "socks"] — not [["shirt", "hat"], ["socks"]]The equivalent with .map().flat() works too, but flatMap does it in a single pass over the data rather than two, and reads as one clear operation rather than two chained ones once the pattern is familiar.
Array.from and generating sequences
Array.from covers a case plain array literals can't — generating an array from a length or an iterable, useful for building a range or transforming array-like objects (like a NodeList from the DOM) into a real array with real array methods:
const range = Array.from({ length: 5 }, (_, i) => i);
// [0, 1, 2, 3, 4]
const nodeArray = Array.from(document.querySelectorAll(".item"));
// a real array, with .map/.filter/etc. — a NodeList alone doesn't have theseThe second argument (a map function, applied to each generated element) is what makes Array.from({ length: n }, ...) a genuine alternative to a manual for loop for building a sequence — no separate empty-array-then-push step required.
Sorting: the one method with a footgun built in
.sort() mutates the original array in place and, without a compare function, sorts elements as strings by default — a genuinely common source of bugs when sorting numbers:
[40, 100, 3, 25].sort();
// [100, 25, 3, 40] — sorted as strings ("1" < "2" < "3" < "4"), not numeric order
[40, 100, 3, 25].sort((a, b) => a - b);
// [3, 25, 40, 100] — correct, with an explicit numeric compare functionAlways pass an explicit compare function for anything other than plain string sorting, and use .toSorted() (the newer, non-mutating counterpart, available in current browsers and Node) instead of .sort() when the original array's order needs to stay untouched — the same mutation-avoidance instinct that makes map/filter preferable to hand-rolled loops applies here too.
Common mistakes
- Using
forEachwhen the intent is actually to transform or filter the array.forEachreturnsundefinedand exists for side effects only — reaching formap/filterwhen you actually want a new array communicates intent more clearly and avoids the temptation to mutate an outer array from inside the callback. - Mutating the original array inside
map's callback instead of returning a new value.mapis meant to be non-destructive; mutating the source array as a side effect inside it makes the code's behavior depend on execution order in a way that's easy to get wrong. - Forgetting
reduce's initial value argument. Without it,reduceuses the array's first element as the initial accumulator and starts iterating from the second — which silently changes behavior (and can throw on an empty array) in a way that's easy to miss in review. - Chaining
.filter().map()when a single.reduce()(or.flatMap()for the filter+map case) would do one pass instead of two — usually not worth the readability tradeoff at small scale, but worth knowing for genuinely large arrays.
Related reading
- The JavaScript Event Loop, Explained With Diagrams — shares tags: javascript, programming (same category).
- How to Migrate a JavaScript Codebase to TypeScript Incrementally — shares tags: javascript, programming.
- TypeScript Utility Types Cheat Sheet — shares tags: javascript, programming.
- Async Python with asyncio: A Practical Introduction — shares tags: programming.
- Big O Notation Without the Math Panic — shares tags: programming.