1

I figured this must be a dup, but I can't find it on SO. Given an object like this:

let obj = { keyA: { keyB: 'hi', keyC: { keyD: null } }, keyE: 'hi' }

Is there a way I can find key paths to a given value, like this:

keyPaths(obj, 'hi') // -> [ 'keyA.keyB', 'keyE' ]
keyPaths(obj)       // -> [ 'keyA.keyB.keyD' ]

I tried to adapt some of the answers that find deep values knowing the key, and I was almost able to adapt this one that finds deep nulls, but I can't figure out how to get the path back, instead of just the deepest key.

goodson
  • 727
  • 2
  • 14
  • 24
  • _"...and I was almost able to adapt this one that finds deep nulls..."_ - You've missed to post that approach... – Andreas Mar 08 '19 at 15:28
  • Am I missing something or should `keyPaths(obj)` return `[ 'keyA.keyB.keyC.keyD' ]`? – Wex Mar 08 '19 at 16:03

3 Answers3

2

I would go with a depth first search like this :

let obj = { keyA: { keyB: 'hi', keyC: { keyD: null } }, keyE: 'hi' }

function keyPaths(parent, value = null, chain) {
  let allResults = [];
  for (const prop in parent) {
    if (parent.hasOwnProperty(prop)) {
      const element = parent[prop];
      const newChain = chain ? chain + '.' + prop : prop;
      if (element === value) {
        allResults.push(newChain);
      }
      else if (Object.keys(prop).length > 1) {
        allResults = [...allResults, ...keyPaths(element, value, newChain)];
      }
    }
  }
  return allResults;
}

console.log(keyPaths(obj, 'hi')) // -> [ 'keyA.keyB', 'keyE' ]
console.log(keyPaths(obj))       // -> [ 'keyA.keyB.keyC' ]

Basically, I check all the properties of the given element for a matching value. If a property doesn't match the value, but has child properties, I recursively call the function, and merge the results from the call iteration and the recursive call.

Seblor
  • 6,947
  • 1
  • 25
  • 46
2

You do this pretty cleanly by using reduce inside a recursive function. The function will return an array, which you can than map() to whatever string values you want.

let obj = { keyA: { keyB: 'hi', keyC: { keyD: null } }, keyE: 'hi' }

function keyPaths(obj, val, path = [] ){
  if (!obj) return 
  return Object.entries(obj).reduce((res, [k, v]) => {
    let p = [...path, k]
    if (v == val) res.push(p)
    else if (v && typeof v == 'object') res.push(...keyPaths(v, val, p))
    return res
  }, [])
}

console.log(keyPaths(obj, 'hi').map(a => a.join('.')))
console.log(keyPaths(obj).map(a => a.join('|')))
Mark
  • 90,562
  • 7
  • 108
  • 148
  • 1
    Thanks for this. I'm going to read it more deeply to understand later. I chose the other which came first. – goodson Mar 08 '19 at 16:09
0

If it's ok to use Lodash+Deepdash, then:

let paths = _(obj).filterDeep((v)=>v=='hi').paths().value();

Codepen is here

Yuri Gor
  • 1,353
  • 12
  • 26