If you have read my previous blog post regarding how to flatten a javascript array, you are already familiar with the step by step approach we used to come up with the final solution. That’s what we will be doing in this tutorial as well. We will start with a simple implementation first and along the way we will end up using:

  • Object.assign
  • recursion
  • concat
  • spread operator
  • map

Here’s the final implementation.

const isPlainObject = function (obj) {
    return Object.prototype.toString.call(obj) === '[object Object]';
};

const flatten = (obj) => {
  return Object.assign(
    {},
    ...function _flatten(obj) {
      return [].concat(...Object.keys(obj)
        .map(k => {
          if (isPlainObject(obj[k])) {
            return _flatten(obj[k]);
          } else {
            return { [k]: obj[k] }   
          }
        })
      )
    }(obj)
  )
}

Testing

You can find the different implementations of flattening a nested object on Codepen. Click on the Console button on the left bottom bar to see the expected output.

Initial implementation

Let’s start simple and see how we can flatten objects only one level deep. We loop through the object keys and perform a check on each key.

If its value is not an object, we add the key into our new flatObj. If it is an object, we loop through the keys of this object and add the key/values into the flat object. Simple, right?

const flattenObject = obj => {
  let flatObj = {};
  Object.keys(obj).forEach(key => {
    if (isPlainObject(obj[key])) {
      Object.keys(obj[key]).forEach(otherKey => {
        flatObj[otherKey] = obj[key][otherKey];
      });  
    } else {
      flatObj[key] = obj[key];
    }
  });
  return flatObj;
}

 

Recursive implementation

Problem with above solution is that it only works for objects one level deep. We will use recursion to be able to solve the problem with any level of depth.

let flattenObject = obj => {
  let flatObj = {};
  Object.keys(obj).forEach(key => {
    if (isPlainObject(obj[key])) {
      Object.assign(flatObj, flattenObject(obj[key]));      
    } else {
      flatObj[key] = obj[key];
    }
  });
  
  return flatObj;
}

For this implementation apart from recursion we also used Object.assign(), so let’s briefly review what Object.assign does.

Object.assign

Object.assign() is used to copy the properties and values of one or more source objects to a target object.

Object.assign(target, ...sources)

It can be used to:

  • clone an object
  • merge objects

Final implementation

Let’s break down what happens in the final implementation.

We use and return Object.assign to merge the nested objects into the empty target object{}.

For a minute let’s ignore the spread operator ... on line 4 and let’s focus on the _flatten recursive function. This function is almost identical with the _flatten function we defined in the ‘Using concat, map & spread operator’ section of how to flatten an Array  The only difference here is that we loop through Object.keys(obj) and we check if the value of each key is an object.

Feel free to check above blog post for further explanation. In a nutshell, we combine concat function with spread operator [].concat(...arr) to get back a flat array one level deep. In order to accommodate indefinitely nested objects, we use map and recursively call _flatten in case the value is an object.

In the end  _flatten method will return an array of flat objects. With the use of spread operator on line 4 we spread out these objects and merge them with Object.assign in a single object.

const flatten = (obj) => {
  return Object.assign(
    {},
    ...function _flatten(obj) {
      return [].concat(...Object.keys(obj)
        .map(k => {
          if (isPlainObject(obj[k])) {
            return _flatten(obj[k]);
          } else {
            return { [k]: obj[k] }   
          }
        })
      )
    }(obj)
  )
}

 

Including path

const flattenPath = (obj) => {
  return Object.assign(
    {},
    ...function _flatten(obj, path = []) {
      return [].concat(...Object.keys(obj)
        .map(k => {
          if (isPlainObject(obj[k])) {
            return _flatten(obj[k], path.concat([k]));
          } else {
            return { [path.concat([k]).join('.')]: obj[k] }   
          }
        })
      )
    }(obj)
  )
}

Testing above function with {ab: {cd: {e:'foo', f:'bar'}, g:'foo2'}} would give back:

flattenPath({ab: {cd: {e:'foo', f:'bar'}, g:'foo2'}})

{
    'ab.cd.e' : 'foo',
    'ab.cd.f' : 'bar',
    'ab.g' : 'foo2'
}