13. Array.prototype.{flat,flatMap}
13.1. Overview
The ES2019 feature Array.prototype.{flat,flatMap}
(by Michael Ficarra, Brian Terlson, Mathias Bynens) adds two new methods to Arrays (to Array<T>.prototype
): .flat()
and .flatMap()
.
13.1.1. .flat()
The type signature of Array<T>.prototype.flat()
is:
.flat(depth = 1): any[]
.flat()
“flattens” an Array: It creates a copy of the Array where values in nested Arrays all appear at the top level. The parameter depth
controls how deeply .flat()
looks for non-Array values. For example:
> [ 1,2, [3,4], [[5,6]] ].flat(0) // no change
[ 1, 2, [ 3, 4 ], [ [ 5, 6 ] ] ]
> [ 1,2, [3,4], [[5,6]] ].flat(1)
[ 1, 2, 3, 4, [ 5, 6 ] ]
> [ 1,2, [3,4], [[5,6]] ].flat(2)
[ 1, 2, 3, 4, 5, 6 ]
13.1.2. .flatMap()
The type signature of Array<T>.prototype.flatMap()
is:
.flatMap<U>(
callback: (value: T, index: number, array: T[]) => U|Array<U>,
thisValue?: any
): U[]
.flatMap()
is the same as first calling .map()
and then flattening the result. That is, the following two expressions are equivalent:
For example:
> ['a', 'b', 'c'].flatMap(x => x)
[ 'a', 'b', 'c' ]
> ['a', 'b', 'c'].flatMap(x => [x])
[ 'a', 'b', 'c' ]
> ['a', 'b', 'c'].flatMap(x => [[x]])
[ [ 'a' ], [ 'b' ], [ 'c' ] ]
> ['a', 'b', 'c'].flatMap((x, i) => new Array(i+1).fill(x))
[ 'a', 'b', 'b', 'c', 'c', 'c' ]
13.2. More information on .flatMap()
Both .map()
and .flatMap()
take a function f
as a parameter that controls how an input Array is translated to an output Array:
- With
.map()
, each input Array element is translated to exactly one output element. That is,f
returns a single value. - With
.flatMap()
, each input Array element is translated to zero or more output elements. That is,f
returns an Array of values (it can also return non-Array values, but that is less common). This is an implementation of.flatMap()
(a simplified version of JavaScript’s implementation that does not conform to the specification):
.flatMap()
is simpler if mapFunc()
is only allowed to return Arrays, but JavaScript doesn’t impose this restriction, because non-Array values are occasionally useful (see the section on .flat()
for an example).
What is .flatMap()
good for? Let’s look at use cases!
13.3. Use case: filtering and mapping at the same time
The result of the Array method .map()
always has the same length as the Array it is invoked on. That is, its callback can’t skip Array elements it isn’t interested in.
The ability of .flatMap()
to do so is useful in the next example: processArray()
returns an Array where each element is either a wrapped value or a wrapped error.
The following code shows processArray()
in action:
.flatMap()
enables us to extract just the values or just the errors from results
:
13.4. Use case: mapping to multiple values
The Array method .map()
maps each input Array element to one output element. But what if we want to map it to multiple output elements?
That becomes necessary in the following example: The React component TagList
is invoked with two attributes.
The attributes are:
- An Array of tags, each tag being a string.
- A callback for handling clicks on tags.
TagList
is rendered as a series of links separated by commas:
Due to .flatMap()
, TagList
is rendered as a single flat Array. The first tag contributes one element to this Array (a link); each of the remaining tags contributes two elements (comma and link).
13.5. Other versions of .flatMap()
13.5.1. Arbitrary iterables
.flatMap()
can be generalized to work with arbitrary iterables:
Due to Arrays being iterables, you can process them via flatMapIter()
:
One benefit of flatMapIter()
is that it works incrementally: as soon as the first input value is available, output is produced. In contrast, the Array-based .flatMap()
needs all of its input to produce its output.
That can be demonstrated via the infinite iterable created by the generator function naturalNumbers()
:
In line A, we extract the first 5 values of infiniteOutput
via destructuring.
13.5.2. Implementing .flatMap() via .reduce()
We can use the Array method .reduce()
to implement a simple version of .flatMap()
:
It depends on your taste, if you prefer the original, more efficient imperative version or this more concise functional version.
13.6. More information on .flat()
This is an implementation of .flat()
(a simplified version of JavaScript’s implementation, that does not conform to the ECMAScript specification):
.flat()
with a depth
of 1 can also be implemented as follows:
.flat(1)
is the same as using .flatMap()
with the identity function (x => x
). That is, the following two expressions are equivalent:
The next subsections cover use cases for .flat()
.
13.6.1. Use case: conditionally inserting values into an Array
The following code only inserts 'a'
if cond
is true
:
Caveat: If you replace either 'a'
or 'b'
with an Array, then you have to wrap it in another Array.
13.6.2. Use case: filtering out failures
In the following example, downloadFiles()
only returns the texts that could be downloaded.
async function downloadFiles(urls) {
const downloadAttempts = await Promises.all( // (A)
urls.map(url => downloadFile(url)));
return downloadAttempts.flat(); // (B)
}
async function downloadFile(url) {
try {
const response = await fetch(url);
const text = await response.text();
return [text]; // (C)
} catch (err) {
return []; // (D)
}
}
downloadFiles()
first maps each URL to a Promise resolving to either:
- An Array with the successfully downloaded text (line C)
- An empty Array (line D)
Promises.all()
(line A) converts the Array of Promises into a Promise that resolves to a nested Array.await
(line A) unwraps that Promise and.flat()
un-nests the Array (line B).
Note that we couldn’t have used .flatMap()
here, because of the barrier imposed by the Promises returned by downloadFile()
: when it returns a value, it doesn’t know yet if it will be a text or an empty Array.
13.7. FAQ
13.7.1. Do .flat() and .flatMap() also flatten iterable elements?
No, only Arrays are flattened:
13.8. Further reading
- “A collection of Scala ‘flatMap’ examples” by Alvin Alexander
- Sect. “Transformation Methods” (which include
.map()
) in “Speaking JavaScript” - Conditionally adding entries inside Array and object literals [loosely related to what is covered in this blog post]