flatMap()
ES2019+Returns a new array formed by applying a given callback function to each element of the array, then flattening the result by one level.
Syntax
array.flatMap(callback(element, index, array), thisArg)Parameters
callback Function Function that produces an element of the new Array
Return Value
A new array with mapped and flattened elements
Examples
const sentences = ['Hello World', 'How are you'];
const words = sentences.flatMap(s => s.split(' '));
console.log(words); 📌 When to Use
Use flatMap() for one-to-many transformations: each input element maps to an array of zero, one, or several outputs, and the results merge into a single flat list. Splitting sentences into words, expanding orders into their line items, generating size/color variants per product, and collecting all tags across a list of posts are all this exact shape. Its second superpower follows from the "zero outputs" case: returning [] drops an element entirely while returning [transformed] keeps it, so one flatMap() pass expresses filter-plus-map - users.flatMap(u => u.active ? [u.email] : []) - without an intermediate array between two chained methods. Positioning it among neighbors: use map() when every element yields exactly one output (wrapping single values in arrays just to satisfy flatMap adds noise); use flat() when the data is ALREADY nested and no per-element transform is needed; use flat(depth) after map() in the rare case a transform produces nesting deeper than one level, because flatMap() flattens exactly one level with no depth parameter, by design. In TypeScript pipelines, flatMap() often replaces awkward reduce-based collectors with a single well-typed expression, inferring the flattened element type automatically.
⚠️ Common Mistakes
Expecting deep flattening. flatMap() flattens exactly ONE level of what the callback returns and offers no depth parameter, so a callback producing [[a, b]] leaves [a, b] nested in the result. If a transform genuinely yields deeper structures, chain .flat(depth) after a plain map() - or reshape the callback so it returns a flat array in the first place.
Writing arr.map(fn).flat() out of habit. It produces the same result as arr.flatMap(fn) but materializes the entire nested intermediate array first, then walks it again to flatten - two passes and an extra allocation for zero benefit. The fused form is also the clearer signal to readers that a one-to-many expansion is happening.
Misreading how non-array returns behave: a bare value from the callback is kept as-is (as if wrapped in a one-element array), so flatMap() degenerates into an expensive map() when the callback never returns arrays. Harmless output-wise, but it usually means the author wanted map(), or forgot the brackets that make the filtering idiom work.
Returning undefined on the "drop this element" branch instead of []. An implicit undefined is a VALUE to flatMap(), not an absence, so the result fills with undefined entries exactly where elements should have vanished. The filtering idiom requires an explicit empty array: condition ? [value] : [].
Splitting strings inside flatMap() without handling empty strings: "a b".split(" ") yields ["a", "", "b"] on the double space, and those empties flow into the flattened result. Trim and filter within the callback - s.split(/\s+/).filter(Boolean) - or word counts and joins go subtly wrong.
✅ Best Practices
Internalize the conditional idiom - return [x] to keep, [] to drop, [a, b] to expand - and flatMap() becomes a single-pass swiss-army transform: arr.flatMap(x => x.valid ? [normalize(x)] : []) filters AND maps with one iteration and one allocated result.
Treat any .map(...).flat() you encounter as a mechanical refactor to flatMap() - same semantics guaranteed by spec, one pass instead of two. The reverse is also useful review guidance: flatMap() whose callback always returns exactly one element should become plain map().
Weigh the filter-map fusion against readability: filter(...).map(...) reads as two obvious steps, flatMap(x => cond ? [f(x)] : []) as one clever one. Prefer the chain in cold code and the fusion where profiling shows the intermediate array matters or where the expansion case (multiple outputs) already forces flatMap().
Use flatMap() to join parent context onto children while expanding: orders.flatMap(o => o.items.map(i => ({ ...i, orderId: o.id }))) yields one flat list of items, each stamped with its parent order id - the denormalized shape tables and CSV exports want.
For extracting one nested array property across a collection, flatMap(o => o.items) is the canonical one-liner - shorter and cheaper than map(o => o.items).flat(), and clearer than a reduce with concat.
⚡ Performance Notes
flatMap() runs one pass over the input and appends each callback result directly into the output, so total cost is O(total output elements) with a single result allocation - versus map().flat(), which allocates the full nested intermediate, then walks it again. The output length is unknowable in advance (each element may contribute any number of items), so V8 grows the result dynamically like it does for filter(); that is inherent to the operation, not a flaw. Per-element, note that the filtering idiom allocates a tiny temporary array ([x] or []) per callback - V8's young-generation GC handles such short-lived objects cheaply, and escape analysis can sometimes eliminate them, but in an extremely hot loop a plain for loop pushing conditionally will still edge it out. Scale intuition: flatMapping 100,000 orders into 400,000 line items is one comfortable pass; the thing to avoid is chaining several expansion steps that each materialize millions of intermediates - fuse them into one callback or stream with generators instead.
🌍 Real World Example
Extracting All Words from Sentences
Both halves of flatMap()'s personality in one example. The first is the classic expansion: each sentence maps to SEVERAL words via split(), and flatMap() merges the per-sentence word arrays into one corpus-style list ready for counting or indexing - map() alone would leave a ragged array of arrays. The second is the conditional-inclusion idiom doing filter-and-extract in a single pass: active users with a real email contribute [user.email], everyone else contributes [], so invalid entries vanish rather than surfacing as null holes. Notice how the empty-array return handles BOTH the inactive user and the missing email with one expression - the branches collapse into the data itself.
const sentences = [
'Hello world',
'JavaScript is awesome',
'Learn to code'
];
const allWords = sentences.flatMap(sentence =>
sentence.split(' ')
);
// ['Hello', 'world', 'JavaScript', 'is', 'awesome', 'Learn', 'to', 'code']
// Filter and map in one pass - get valid emails from users
const users = [{email: 'a@b.com', active: true}, {email: null, active: true}, {email: 'c@d.com', active: false}];
const activeEmails = users.flatMap(user =>
user.active && user.email ? [user.email] : []
);
// ['a@b.com']