Search code examples
javascriptrecursionjavascript-objectsflatten

Recursively flatten a deeply-nested mix of objects and arrays


I'm trying to flatten a data object that contains mixed content (JavaScript within a React application). I just need the keys of every parent and child in the object; I don't necessarily need the values (though having them present wouldn't be a deal-breaker).

I've searched for over a week to find a solution that would fit my use case, but everything I've tried has fallen short, including vanilla JavaScript, Lodash, Underscore, and flat (NPM package).

In every case, I either get a shorter list than I expect (because I'm only getting the parents), or I get a fully-flattened object with dot notation-delimited objects, which is useless to me.

I know there are lots of questions & answers pertaining to this topic, but nothing I've seen matches my use case, and I can't seem to wrap my head around the problem.

Here is a sample of my data structure:

const sampleData = {
  coyotes: '',
  armadillos: false,
  wombats: [''],
  geckos: 0,
  dogs: {
    beagle: '',
    bulldog: ''
  },
  wolves: [
    {
      type: '',
      color: '',
      range: '',
      status: {
        endangered: true,
        protected: false
      }
    }
  ],
  cats: {}
}

Here is what I (ideally) want back:

result = {coyotes, armadillos, wombats, geckos, dogs, beagle, bulldog, wolves, type, color, range, status, endangered, protected, cats}

Here is the way I'd like to call the flattening method:

flattenData(sampleData)

Here's what I have so far:

const flattenData = (obj) =>
      Object.assign(
        {},
        ...(function _flatten (o) {
          return [].concat(...Object.keys(o)
            .map(k =>
              typeof o[k] === 'object' ? _flatten(o[k]) : (Number([k]) !== 0 && { [k]: o[k] })
            )
          )
        }(obj))
      )

...which produces:

{
    "coyotes": "",
    "armadillos": false,
    "geckos": 0,
    "beagle": "",
    "bulldog": "",
    "type": "",
    "color": "",
    "range": "",
    "endangered": true,
    "protected": false
}

As you'll note, some parents are missing (wombats, dogs, wolves and status), and an empty object is missing (cats).


Solution

  • Here's a simple recursion:

    const deepKeys = (o) => 
      Array .isArray (o)
        ? o .flatMap (deepKeys)
      : Object (o) === o
        ? Object .entries (o) .flatMap (([k, v]) => [k, ... deepKeys (v)])
      : []
    
    const sampleData = {coyotes: '', armadillos: false, wombats: [''], geckos: 0, dogs: {beagle: '', bulldog: ''}, wolves: [{type: '', color: '', range: '', status: {endangered: true, protected: false}}], cats: {}}
    
    console .log (deepKeys (sampleData))
    .as-console-wrapper {max-height: 100% !important; top: 0}

    We need to process a little differently for arrays, objects, and other types, but each one is fairly simple.