Getting To Know JavaScript for React: Array.flat()

Alexander Jeffcott

An in-depth look at JavaScript ES10 / ECMAScript 2019

The 10th edition of JavaScript, ECMAScript 2019, was published in June 2019. Here’s one new addition which could well remove a dependency or two or better yet replace a couple of lines of quite dense code.

Array.Flat()

This array method takes a single optional “depth” argument (which defaults to 1should it be undefined). It creates a new array with all sub-array elements concatenated into it recursively up to the value of ‘depth’. It is worth bearing in mind that it will also remove array “holes” where there are empty slots in the array.

Here is the new method and its output compared to similar in Lodash and Ramda:

const arr = [1, 2, [3, 4], 5, [6, [7, 8, [9, [10, 11], 0]]]]// with the new feature
arr.flat()
arr.flat(2)
arr.flat(Infinity)// with Lodash
_.flatten(arr)
_.flattenDepth(arr, 2)
_.flattenDeep(arr)// with Ramda
R.unnest(arr);
// no function which takes a depth arg
R.flatten(arr)// the output
// [1, 2, 3, 4, 5, 6, [7, 8, [9, [10, 11], 0]]]
// [1, 2, 3, 4, 5, 6, 7, 8, [9, [10, 11], 0]]
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0]
It is worth considering what achieving the same result without this method or a utility library would look like:

// just one level looks pretty fine and could be done like this:
const flatSingle = _arr =>
  _arr.reduce((acc, cur) =>
    _acc.concat(cur), [])// or like this:
const flatSingle = _arr => [].concat(..._arr)flatSingle(arr)// flatten to a specified level (defaults to 1)
const flattenToDepth = (_arr, depth = 1) =>
  _arr.reduce((acc, cur) =>
    a.concat(depth > 1 && Array.isArray(cur)
      ? flatten(cur, depth - 1)
      : cur
    ), [] )flattenToDepth(arr)// deeply flattening gets a bit more involved:
const flattenDeep = _arr =>
  _arr.reduce((acc, cur) =>
    Array.isArray(cur)
      ? acc.concat(flattenDeep(cur))
      : acc.concat(cur), [])flattenDeep(arr)




It is worth considering what achieving the same result without this method or a utility library would look like:

// just one level looks pretty fine and could be done like this:
const flatSingle = _arr =>
  _arr.reduce((acc, cur) =>
    _acc.concat(cur), [])// or like this:
const flatSingle = _arr => [].concat(..._arr)flatSingle(arr)// flatten to a specified level (defaults to 1)
const flattenToDepth = (_arr, depth = 1) =>
  _arr.reduce((acc, cur) =>
    a.concat(depth > 1 && Array.isArray(cur)
      ? flatten(cur, depth - 1)
      : cur
    ), [] )flattenToDepth(arr)// deeply flattening gets a bit more involved:
const flattenDeep = _arr =>
  _arr.reduce((acc, cur) =>
    Array.isArray(cur)
      ? acc.concat(flattenDeep(cur))
      : acc.concat(cur), [])flattenDeep(arr)

As can be seen above, it is a good opportunity to ditch a dependency (or 3 if you are using flatten, flattenDepth and flattenDeep), however, there may be cases where you still want to keep the utility library method. Consider the following case: what would be a way to replace flatten with flat that would make it worth saving the import cost?

import flow from 'lodash/fp/flow'
import flatten from 'lodash/fp/flatten'
import sortBy from 'lodash/fp/sortBy'
import compact from 'lodash/fp/map'const orderStringsFlatly = _arr =>
  flow(
    flatten,
    map(x => x + '')
    sortBy(x => x),
  )(_arr)orderStringsFlatly(arr)

While there may be ways to do this, a quite fair assumption on the part of the reader is that the entire flow (if you will forgive the pun) is within the remit of lodash/fp. I, personally, would be surprised to see this and would find myself checking that the other methods are part of lodash/fp or whether they could or should be replaced as well. In other words, it could well add mysterious overhead simply by adding a potential for doubt, not in the code per se, but in the minds of the developers who read it.

And Another Thing, in Case You Were Wondering

A question that popped into my mind was what happens if there are other data structures in the nesting? Well, here you go!

const arrWithDictionariesAndLists = [
  {a: 1, b: true, c: [[3, 4]]}, 5, [6, [7, 8, [9, [10, 11], 0]]]
]arrWithDictionariesAndLists.flat()
arrWithDictionariesAndLists.flat(2)
arrWithDictionariesAndLists.flat(Infinity)// [{a:1, b:true, c:[[3, 4]]}, 5, 6, [7, 8, [9, [10 ,11], 0]]]
// [{a:1, b:true, c:[[3, 4]]}, 5, 6, 7, 8, [9, [10 ,11], 0]]
// [{a:1, b:true, c:[[3, 4]]}, 5, 6, 7, 8, 9, 10 ,11, 0]

This is probably as we would expect. Non-list elements are essentially ignored and are returned to the new array unchanged. This means, of course, that lists inside a dictionary would also be unchanged.

Where Does it Work?

As you can see from the table below, taken from an ECMAScript implementation table, it works in the current Firefox, Chrome, Edge, Safari, Node v12 (LTS in 22 days at the time of writing) which is fine if your application can assume that the users’ systems are up-to-date.

ECMAScript implementation table for flat and flatMap
ECMAScript implementation of flat and flatMap

Generally, we can’t and don’t. So if we want this to run on last year’s browsers, we are likely going to need to make sure that our transpiler can deal with it, and if you are using Babel then this means Babel7 with JSCore3 for polyfills. This works a bit differently from how it worked before, check out the Babel docs here to see what this means for your project.

Further reading

Find out about the surprising drama behind the naming of this feature and how it came oh so close to being Array.smoosh().