1
\$\begingroup\$

Problem Statement

I need to access a deeply nested object. I could've used a 3rd party util such as Lodash but insisted myself on writing a Vanilla JS solution with a recursive strategy.

Code

Can this solution be improved with a better and more efficient approach? Is there any edge case which has been missed and could break the code?

const obj = {
  foo: {
    bar: {
      baz: "value"
    }
  }
};

const getPathValue = (object, path) => {  
  if (
    // ======= object checks ======= //
    object === null ||
    object === undefined ||
    typeof object !== "object" ||
    // ======= path checks ======= //
    path === null ||
    path === undefined ||
    path.length === 0
  ) {
    return undefined;
  }

  // key -> current key
  // rest -> rest of the keys in path
  const [key, ...rest] = path;

  // value of current key
  const value = object[key];

  // if rest of the keys are exhausted &
  // a value is found, return the value
  if (rest.length === 0 && value) {
    return value;
  }

  // as rest of key are not exhaused yet
  // in the path, keep traversing deeper
  return getPathValue(value, rest);
};

// ======= test cases with object edge cases ======= //
console.log(getPathValue()); // undefined
console.log(getPathValue(null)); // undefined
console.log(getPathValue(undefined)); // undefined

// ======= test cases with path edge cases ======= //
console.log(getPathValue(obj, [])); // undefined
console.log(getPathValue(obj, null)); // undefined
console.log(getPathValue(obj, [1, 2, 3])); // undefined
console.log(getPathValue(obj, ["non exisiting key"])); // undefined
console.log(getPathValue(obj, ["foo", "non exisiting key"])); // undefined
console.log(getPathValue(obj, ["foo", "bar", "baz", "non exisiting key"])); // undefined
console.log(getPathValue(obj, ["foo", "non exisiting key", "bar", "baz"])); // undefined

// ======= normal test case ======= //
console.log(getPathValue(obj, ["foo", "bar", "baz"])); // value

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Recursion

There are many reasons to avoid recursion in JavaScript.

  • Recursion is an inefficient way to implement simple loops.

    Generally you use recursion to follow tree like paths where the path is dynamic or unknown, in this case the path is pre-determined and there is no need to stack the previous loop state as you iterate.

  • A strong argument to NEVER use recursion is that JS has a limited call stack, but that same argument also means NEVER call a function (You may be at the end of the call stack) which is a ridiculous requirement.

    Until JS supports tail call optimization you should always use a loop in preference to recursion

Argument vetting

The argument vetting too complex and can be simplified to typeof object === "object" && object !== null && Array.isArray(path) see rewrite.

Note "null is bad" Good JS should never set a value to null and thus avoid the ridiculous need to test if an object is null, unfortunately we must deal with the designers of the DOM and CSS that seam to have missed what null means in JS.

Cyclic safe

As a rule iterating paths should be wary of cyclic paths, however this is only a concern if the search is open ended (able to loop infinitely) and thus not an issue in this case.

Inconsistent return

If passed an empty array your example returns undefined which does not make sense if compared to an incomplete path that will return an Object or value. An empty path is equivalent to an incomplete path.

For example the objects

const o1 = {A: { B: {C: 0}}};
const o2 = {B: {C: 0}};

The root object can not be found

getPathValue(o1, ["A"]);// returns {B: {C: 0}};
getPathValue(o1, []);   // returns undefined; One would expect {A: { B: {C: 0}}}
getPathValue(o2, []);   // returns undefined; One would expect {B: {C: 0}}
getPathValue(getPathValue(o1, ["A"]), []); // returns undefined; One would expect {B: {C: 0}}

Rewrite

The rewrite

  • Returns undefined if a path can not be found.

  • Returns the object at the end of the path. eg getPathValue(obj, []) returns obj not undefined

  • Will also follow an index path into arrays.

  • Uses ?? (Nullish coalescing operator) to simplify search along path and as such will return undefined rather than null. Fitting "null is bad" rule for JS.

  • If second argument is not an array the result will be undefined.


function valueAtPath(obj, path) {  
    var i = 0;  
    if (typeof obj === "object" && obj !== null &&  Array.isArray(path)) {
        while (i < path.length && obj !== undefined) { obj = obj[path[i++]] ?? undefined }
        return obj;
    }
}

\$\endgroup\$
2
  • \$\begingroup\$ Had o1 been accessed dynamically with o1[] or o1[''], it would have thrown an error or returned undefined value respectively. So, respecting this behaviour, I expect getPathValue(o1, []) to return undefined likewise. What are your thoughts on this? \$\endgroup\$ Jul 15, 2021 at 14:00
  • \$\begingroup\$ @VinaySharma An array with an empty string is not an empty array. What you return for these cases is up to you as there is no right or wrong. My view is... You do not provide a way to return the root object and thus would have to add a statement to the call if you required the root object. The function solves a path, if you take no steps along a path you in effect remain at the start (root) of the path. If you attempt to step on an undefined path eg named "" empty string, then you have moved to an undefined location. \$\endgroup\$
    – Blindman67
    Jul 15, 2021 at 14:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.