JavaScript flat() and flatMap() Explained
flat() flattens nested arrays by a given depth; flatMap() maps then flattens one level in a single pass. How each works and when to pick one.
Array.prototype.flat() flattens nested arrays into a single array up to a given depth, and Array.prototype.flatMap() does a map() and a one-level flat() in a single pass. Both exist to solve the same recurring annoyance: code that produces an array of arrays when you actually wanted one flat array.
flat(): collapsing nested arrays
flat() takes an optional depth argument (default 1) and returns a new array with sub-array elements concatenated up to that depth:
const nested = [1, [2, 3], [4, [5, 6]]];
nested.flat(); // [1, 2, 3, 4, [5, 6]] — depth 1 (default)
nested.flat(2); // [1, 2, 3, 4, 5, 6] — depth 2
nested.flat(Infinity); // fully flattens any depth
Each level of nesting needs its own unit of depth. This matters for data that comes from sources with unpredictable nesting — API responses that group items, or recursive tree structures — where Infinity is often the pragmatic choice unless you specifically want to preserve some structure.
flat() also has a useful side effect: it removes empty slots from sparse arrays, which makes arr.flat() a common idiom for compacting an array that has holes, even when there’s no nesting to collapse.
flatMap(): map, then flatten one level
flatMap() is equivalent to calling map() followed by flat(1), but it runs as a single pass rather than allocating an intermediate array:
const sentences = ["hello world", "how are you"];
sentences.map(s => s.split(" "));
// [["hello", "world"], ["how", "are", "you"]]
sentences.flatMap(s => s.split(" "));
// ["hello", "world", "how", "are", "you"]
The callback in flatMap() can return either a single value or an array — non-array returns pass through unchanged, so it works as a drop-in replacement for map() when a callback occasionally needs to expand one input into several outputs (or none at all).
A common pattern: filter and map together
Because a callback can return an empty array, flatMap() doubles as a way to filter and transform in one step, which normally takes a .filter().map() chain (see map, filter, reduce for the individual methods):
const numbers = [1, 2, 3, 4, 5, 6];
// Only keep even numbers, doubled — filter + map in one call
numbers.flatMap(n => (n % 2 === 0 ? [n * 2] : []));
// [4, 8, 12]
This isn’t necessarily faster or more readable than a separate .filter().map() chain — it’s a matter of taste and how often the pattern recurs in your codebase. Reach for it when the “return nothing” case is a natural fit, not as a rule.
What came before: reduce() and manual loops
Before flat() and flatMap() were standardized, flattening nested arrays meant either a manual recursive function or a reduce() call that concatenated as it went:
const nested = [[1, 2], [3, 4], [5]];
nested.reduce((acc, arr) => acc.concat(arr), []);
// [1, 2, 3, 4, 5]
That works, but it’s noticeably more code for a common operation, and it’s easy to get subtly wrong — forgetting the initial [] accumulator, or using push instead of concat and mutating in place when you didn’t mean to. flat() and flatMap() exist specifically to replace this pattern with something that reads as intent rather than mechanism. If you’re reviewing older code and see a reduce() call whose only job is concatenating sub-arrays together, it’s almost always a safe, direct swap for flat().
Working with other array methods
Both methods return plain arrays, so they compose naturally with the rest of the array method toolkit covered in map, filter, reduce. A common real-world shape is fetching a paginated or grouped API response, then using flatMap() to both extract and flatten the relevant field in one step:
const groups = [
{ category: "fruit", items: ["apple", "pear"] },
{ category: "veg", items: ["carrot"] },
];
groups.flatMap(g => g.items);
// ["apple", "pear", "carrot"]
This avoids an intermediate .map(g => g.items) call that would leave you with an array of arrays needing a separate .flat() pass — one method call captures the whole operation.
flat() vs flatMap(): when to use which
flat() | flatMap() | |
|---|---|---|
| Input | Already-nested array | Flat array + a callback that may return arrays |
| Transforms elements | No | Yes |
| Depth control | Any depth via argument | Always exactly one level |
| Typical use | Flattening data you received nested | Mapping where each input can expand to 0, 1, or many outputs |
If you already have nested data and just need it flat, use flat(). If you’re producing the nesting yourself as part of a transform — splitting strings, expanding one record into several — flatMap() avoids the intermediate array map() would otherwise allocate.
A note on performance
For small to medium arrays, the difference between map().flat() and flatMap() is not something you’ll notice. The real cost to watch is depth: flat(Infinity) on deeply or unpredictably nested data does real recursive work, so if you’re processing untrusted or very large nested structures, prefer a bounded depth over Infinity where you can reason about the actual shape of the data.
The takeaway
flat() collapses nested arrays to a chosen depth; flatMap() maps and flattens one level in a single pass, and doubles as a compact filter-and-map when a callback can return zero, one, or several results. Reach for flat() when you already have nested data, and flatMap() when your own transform is what’s producing the nesting.
Tagged
Keep reading
Takina · · 4 min read Object.groupBy() in JavaScript, Explained
Object.groupBy() buckets array items by a key you compute, replacing the reduce() boilerplate developers have written for years. How it works.
Takina · · 4 min read Web Locks API: Coordinating Work Across Browser Tabs
The Web Locks API lets JavaScript acquire named locks shared across tabs, so only one tab does work like a token refresh or a write at a time.
Takina · · 4 min read Svelte 5 Runes Explained
Svelte 5 runes like $state and $derived replace the old reactive-assignment magic with explicit function calls that work anywhere in a file.