Hi there! After my 18 months break due to maternity leave I am back into Javascript coding again! Yayyy, I am so excited about that, so I decided to start refreshing my knowledge with something as simple and basic as flattening a Javascript Array.

ES2019 introduced two new methods to the Array prototype: flat and flatMap. They are both very useful to what we want to do: flatten an array. But I am not going to use any of these two methods in this tutorial, since my intent was mainly to play around with ES6 features I haven’t used for such a long time. I had so much fun coding this and I wanted to share my learning experience with you. I started simple and ended up with this one liner ES6 solution.

const flatten = arr => 
  (_flatten = arr => [].concat(...arr.map(elem => Array.isArray(elem) ? _flatten(elem) : elem)))(arr);

flatten([ [ [ [1], 2], 3], [4], [], [[5]]])); // [1, 2, 3, 4, 5]
flatten(['abc', ['def', ['ghi', ['jkl']]]])); // ["abc", "def", "ghi", "jkl"]

Does it look simple and basic? At first glance definitely not. In the eyes of a beginner developer it looks complex and unreadable. I absolutely agree.  But if you understand how arrow functions, spread syntax, recursion, map and concat functions work, then this code will start looking simple and readable.

Our goal for today is to learn how to break down a complex problem into smaller ones. We will start with a simple solution first and along the way we will end up using:

  • recursion
  • map
  • concat
  • spread operator
  • immediately invoked named arrow functions

So let’s get started!

But wait! Testing first

Right! Before we start, let’s quickly test our code. All you need is your browser. Open your Inspector, copy the above code snippet and paste it in the console tab. Whenever you need to test or learn something new, just open the Inspector and start typing. If you create for example a variable let name = "Panagiota"; and then type name and dot (name.), the browser will show a popup with all the available methods you can use on this variable. And that’s the best way you can learn and practise the Javascript syntax. Go ahead and open your inspector!

As an alternative you can find my four different implementations of flattening an array on Codepen. Click on the Console button on the left bottom bar to see the expected output.

Here are the two arrays I used as an input to the flatten function and corresponding output.

Input: [ [ [ [1], 2], 3], [4], [], [[5]]] -> Output: [1, 2, 3, 4, 5]

Input: [‘abc’, [‘def’, [‘ghi’, [‘jkl’]]]] ->  Output: [“abc”, “def”, “ghi”, “jkl”]

Feel free to try more test cases with the flatten function if you want. Now it’s finally time to get into coding!

Initial implementation

This problem is easier to understand step by step. Let’s see how we can flatten arrays only one level deep. The idea is simple. We loop through the input array and we perform a check on each element.

If the element is not an array we push it into our new result array. If it is an array, we loop through the elements of this array and push each element into the result array.

 

const flatten = arr => {
  let result = [];
  arr.forEach(elem => {
    if (Array.isArray(elem)) {
      elem.forEach(nestedElem => {
        result.push(nestedElem);
      })
    } else {
      result.push(elem);
    }
  });
  return result;
};

If we test above implementation with following input, here’s what we get:

Input: [1, [2, 3], 4] -> Output:  [1, 2, 3, 4]

Input: [1, [2, [3], 4]] -> Output: [1, 2, [3], 4]

Recursive implementation

Problem with above solution is that it only works for arrays one level deep. How can we solve this problem with any level of depth? Recursion comes to the rescue.

const flatten = arr => {
  let result = [];
  function _flatten(arr) {
    arr.forEach(elem => {
      if (Array.isArray(elem)) {
        _flatten(elem);
      } else {
        result.push(elem);
      }
    });
  }
  
  _flatten(arr);
  return result;
};

To be able to flatten arrays that are indefinitely nested, we need to repeat the process of looping through the array and checking against each element. That’s what the _flatten recursive function does.

If we test above implementation with following input, we get the expected result:

Input: [1, [2, [3], 4]] -> Output: [1, 2, 3, 4]

Using concat, map & spread operator

Let’s move on to our next implementation! We still use our _flatten recursive function, but we make use of concat, map and spread operator as well.

const flatten = arr => {
  return function _flatten(arr) {
    return [].concat(...arr.map(elem => {
      if (Array.isArray(elem)) {
        return _flatten(elem);
      } else {
        return elem;
      }     
    }));  
  }(arr);
};

concat offers a way to add new items to the end of an array without any mutating side effects. The method is called on an array and then another array is provided as the argument to concat, which is added to the end of the first array. It returns a new array and does not mutate (change) either of the original arrays. Here’s an example:

[1, 2, 3].concat([4, 5, 6]);

The returned array would be [1, 2, 3, 4, 5, 6].

We could use [].concat([1, 2, 3], [4, 5, 6]) and we would get back the same result.

Spread syntax (...) allows an array expression in a function call to be expanded in more arguments. I will go into more detail regarding spread operator in another post, but for now let’s keep in mind that the spread operator only works in-place, like in an argument to a function or in an array literal. Let’s use it as an argument to the concat method.

[].concat(...[1, [2,3], 4])

[].concat(...[1, [2,3]], [4, 5, 6])

[].concat(...[1, [2,3]], ...[4, [5], 6])

Above commands would all result in the expected flat array  [1, 2, 3, 4, 5, 6]. That’s great, we can combine concat function with spread operator [].concat(...arr) and we get back a flat array.

But this goes only one level deep. If we had two levels for example, it wouldn’t flatten the array the way we want:

[].concat(...[1, [2,[3]], 4]) // [1, 2, [3], 4]

We will try to solve the problem by adding map into the game! If we have an array one level deep we can do following.

let arr = [1, [2, 3], 4];
[].concat(...arr.map(elem => elem)); // [1, 2, 3, 4]

We will get [1, 2, 3, 4], since map returns an array with transformed values (in this case we don’t transform any values, we just return them) and the spread operator on the return value (an array) will break up the array into a list of values.

In order to accommodate an array two levels deep, we use map, we check if the value is an array and we transform it accordingly (we flatten it) with the command we’ve seen before [].concat(...elem)

[].concat(...arr.map(elem => {
    if (Array.isArray(elem)) {
        return [].concat(...elem);
    } else { 
        return elem;
    }
}))

But the problem with [].concat(...elem) is it goes only one level deep.We can solve this issue by recursively calling our _flatten method when the element is an array. That’s how we ended up with the final version of this implementation.

 

const flatten = arr => {
  return function _flatten(arr) {
    return [].concat(...arr.map(elem => {
      if (Array.isArray(elem)) {
        return _flatten(elem);
      } else {
        return elem;
      }     
    }));  
  }(arr);
};

You’ve seen how we broke down our initial problem into smaller ones and how we managed to solve our problem step by step. Prerequisite for this was to understand how concat, map and spread operator work! And recursion of course.

I highly encourage you to open the console tab in your Inspector and start playing with the methods we’ve learnt: concat, map and spread operator. While experimenting with spread operator, use it in an argument to a function call or in an array literal. The following code will not work, since spread operator doesn’t work in place:

const spreaded = ...[1, [2, 3], 4];

More details about spread operator in another post! Let’s finalize now our solution using arrow functions.

Using arrow functions

In this version of our implementation we’ve done two more changes:

  • remove the curly brackets and the return statement inside the map function
  • use the ternary operator

 

const flatten = arr => function _flatten(arr) {
    return [].concat(...arr.map(elem => Array.isArray(elem) ? _flatten(elem) : elem));
  }(arr);

Immediately Invoked Named Arrow Function Expression

This final version is probably something you will not use often. Personally I’ve never used it before. I was just curious to see how I can implement my solution in just one line. I had to assign my arrow function to the _flatten variable, wrap it in parenthesis and immediately invoke it. And here’s how we ended up in the final solution.

 

const flatten = arr => 
  (_flatten = arr => [].concat(...arr.map(elem => Array.isArray(elem) ? _flatten(elem) : elem)))(arr);

I hope you enjoyed this tutorial. It was mainly for learning purposes and a quick refresh of the concat method and spread operator. There are of course more ways to flatten an array. In this tutorial you can read how to flatten an array with the use of reduce.