Search code examples
javascriptrecursionlodash

Deep key structure based on recursion


I've been using lodash for a while now and I really love the _.set and _.get methods.

I'm trying to solve a problem to get the deep key paths whose final value is a string, but when I'm too dumb for it. Spend 3 hours on it and can't find a perfect solution:

const myObject = {
  a: 'myObject.a',
  b: {
    ba: 'myObject.b.ba',
    bb: ['myObject.b.bb[0]'],
  },
  c: [
    { ca: 'myObject.c[0].ca' },
  ],
};

So I have myObject (that's far more nested in real life) and I want to get paths to the values, but only the final one.

The method would look like getDeepPaths(myObject) and would return in this case: ['myObject.a', 'myObject.b.ba', 'myObject.b.bb[0]', 'myObject.c[0].ca' ]

Did anyone solve something like this before?


Solution

  • Recursion is actually not that hard. Here's how you could solve this problem:

    const myObject = {
      a: 'myObject.a',
      b: {
        ba: 'myObject.b.ba',
        bb: ['myObject.b.bb[0]'],
      },
      c: [
        { ca: 'myObject.c[0].ca' },
      ],
    };
    
    
    var stringLeaves = function(path, obj) {
    
      if (typeof obj === 'string') {
        return [path]
      }
    
      return Object.keys(obj)
              .filter(k => obj.hasOwnProperty(k))
              .map(k => stringLeaves(path + '.' + k, obj[k]))
              .reduce((a,x) => a.concat(x), []); // this line flattens the array
    };
    
    console.log(stringLeaves('myObject', myObject));

    The work is done by the stringLeaves function. In this function:

    • if the obj passed in as a parameter is a string, then just return the current path.
    • otherwise we assume that the object is an array, or a generic object, in which case we iterate through its properties:
      • for each property, call stringLeaves recursively, by passing in the adjusted path (current path + the new property name) and the object/value that resides at that particular key.

    The convention of the function is that it returns an array of all possible matches. This is why:

    • for scalar string values I return an array (to keep things consistent)
    • I have the .reduce((a,x) => a.concat(x), []); line: to transform an array of arrays into one array that consists of all the values present in the original arrays.

    Note that the function cannot deduce that your object is called myObject, so I passed that name as an initial path.