Search code examples
javascriptjavascript-objectsnested-object

Retrieve paths of deepest properties in an object


I am trying to find a way to dynamically create an array that contains the paths of the deepest properties in a nested object. For example, if my object is the following:

{
    userName: [],
    email: [],
    name: {
        fullName: [],
        split: {
            first: [],
            last: []
        }
    },
    date: {
        input: {
            split: {
                month: [],
                year: []
            },
            full: []
        },
        select: {
            month: [],
            year: []
        }
    }
};

I would need an array to contain something like:

["userName", "email", "name.fullName", "name.split.first",...]

Are there any built-in or external libraries that do this automatically? I was trying to use Object.keys on the parent object but this only returns the direct children properties.


Solution

  • You can use Array.prototype.flatMap for this -

    const d =
      {userName:[],email:[],name:{fullName:[],split:{first:[],last:[]}},date:{input:{split:{month:[],year:[]},full:[]},select:{month:[],year:[]}}}
    
    const main = (o = {}, path = []) =>
      Array.isArray(o) || Object(o) !== o
        ? [ path ]
        : Object
            .entries(o)
            .flatMap(([ k, v ]) => main(v, [...path, k ]))
            
    console.log(main(d))

    Output

    [ [ "userName" ]
    , [ "email" ]
    , [ "name", "fullName" ]
    , [ "name" ,"split", "first" ]
    , [ "name", "split", "last" ]
    , ...
    ]
    

    If you want the paths to be "a.b.c" instead of [ "a", "b", "c" ], use .map and Array.prototype.join -

    const d =
      {userName:[],email:[],name:{fullName:[],split:{first:[],last:[]}},date:{input:{split:{month:[],year:[]},full:[]},select:{month:[],year:[]}}}
    
    const main = (o = {}, path = []) =>
      Array.isArray(o) || Object(o) !== o
        ? [ path ]
        : Object
            .entries(o)
            .flatMap(([ k, v ]) => main(v, [...path, k ]))
            
    console.log(main(d).map(path => path.join(".")))

    Output

    [
      "userName",
      "email",
      "name.fullName",
      "name.split.first",
      "name.split.last",
      "date.input.split.month",
      "date.input.split.year",
      "date.input.full",
      "date.select.month",
      "date.select.year"
    ]
    

    If you do not want to rely on Array.prototype.flatMap because it is not supported in your environment, you can use a combination of Array.prototype.reduce and Array.prototype.concat -

    const d =
      {userName:[],email:[],name:{fullName:[],split:{first:[],last:[]}},date:{input:{split:{month:[],year:[]},full:[]},select:{month:[],year:[]}}}
    
    const main = (o = {}, path = []) =>
      Array.isArray(o) || Object(o) !== o
        ? [ path ]
        : Object
            .entries(o)
            .reduce // <-- manual flatMap
              ( (r, [ k, v ]) =>
                  r.concat(main(v, [...path, k ]))
              , []
              )
            
    console.log(main(d).map(path => path.join(".")))


    Or you could polyfill Array.prototype.flatMap -

    Array.prototype.flatMap = function (f, context)
    { return this.reduce
        ( (r, x, i, a) => r.concat(f.call(context, r, x, i, a))
        , []
        )
    }
    

    Is there a way to access any of those properties' value? Eg. "d.name.split.first" using the returned array at position 3?

    We can write a lookup function that accepts an object, o, and a dot-separated string, s, that returns a value, if possible, otherwise returns undefined if s is unreachable -

    const d =
      {userName:[],email:[],name:{fullName:[],split:{first:[],last:[]}},date:{input:{split:{month:[],year:[]},full:[]},select:{month:[],year:[]}}}
      
    const lookup = (o = {}, s = "") =>
      s
        .split(".")
        .reduce
          ( (r, x) =>
              r == null ? undefined : r[x]
          , o
          )
     
    console.log(lookup(d, "name.split"))
    // { first: [], last: [] }
    
    console.log(lookup(d, "name.split.first"))
    // []
    
    console.log(lookup(d, "name.split.zzz"))
    // undefined
    
    console.log(lookup(d, "zzz"))
    // undefined