26

I have json based data structure with objects containing nested objects. In order to access a particular data element I have been chaining references to object properties together. For example:

var a = b.c.d;

If b or b.c is undefined, this will fail with an error. However, I want to get a value if it exists otherwise just undefined. What is the best way to do this without having to check that every value in the chain exists?

I would like to keep this method as general as possible so I don't have to add huge numbers of helper methods like:

var a = b.getD();

or

var a = helpers.getDFromB(b);

I also want to try to avoid try/catch constructs as this isn't an error so using try/catch seems misplaced. Is that reasonable?

Any ideas?

1

16 Answers 16

28

ECMAScript2020, and in Node v14, has the optional chaining operator (I've seen it also called safe navigation operator), which would allow your example to be written as:

var a = b?.c?.d;

From the MDN docs:

The optional chaining operator (?.) permits reading the value of a property located deep within a chain of connected objects without having to expressly validate that each reference in the chain is valid. The ?. operator functions similarly to the . chaining operator, except that instead of causing an error if a reference is nullish (null or undefined), the expression short-circuits with a return value of undefined. When used with function calls, it returns undefined if the given function does not exist.

1
  • This is the most relevant and readable solution
    – TechWisdom
    Commented Dec 21, 2021 at 11:58
19

Standard approach:

var a = b && b.c && b.c.d && b.c.d.e;

is quite fast but not too elegant (especially with longer property names).

Using functions to traverse JavaScipt object properties is neither efficient nor elegant.

Try this instead:

try { var a = b.c.d.e; } catch(e){}

in case you are certain that a was not previously used or

try { var a = b.c.d.e; } catch(e){ a = undefined; }

in case you may have assigned it before.

This is probably even faster that the first option.

9

ES6 has optional chaining which can be used as follows:

const object = { foo: {bar: 'baz'} };

// not found, undefined
console.log(object?.foo?.['nested']?.missing?.prop)

// not found, object as default value
console.log(object?.foo?.['nested']?.missing?.prop || {})

// found, "baz"
console.log(object?.foo?.bar)

This approach requires the variable "object" to be defined and to be an object.

Alternatively, you could define your own utility, here's an example which implements recursion:

const traverseObject = (object, propertyName, defaultValue) => {
  if (Array.isArray(propertyName)) {
    return propertyName.reduce((o, p) => traverseObject(o, p, defaultValue), object);
  }

  const objectSafe = object || {};

  return objectSafe[propertyName] || defaultValue;
};

// not found, undefined
console.log(traverseObject({}, 'foo'));

// not found, object as default value
console.log(traverseObject(null, ['foo', 'bar'], {}));

// found "baz"
console.log(traverseObject({foo: {bar:'baz'}}, ['foo','bar']));

5

You can create a general method that access an element based on an array of property names that is interpreted as a path through the properties:

function getValue(data, path) {
    var i, len = path.length;
    for (i = 0; typeof data === 'object' && i < len; ++i) {
        data = data[path[i]];
    }
    return data;
}

Then you could call it with:

var a = getValue(b, ["c", "d"]);
4
  • Given that a.b === a['b'] I assume that using strings to reference objects is considered best practice in JS?
    – Hubris
    Commented Aug 12, 2013 at 1:43
  • @Hubris - Property names are always strings. The dot notation is considered "syntactical sugar" (which doesn't even work if the property name is a reserved word or not a valid JS identifier).
    – Ted Hopp
    Commented Aug 12, 2013 at 1:44
  • Ok, that's what I thought. Awesome. Thank you :)
    – Hubris
    Commented Aug 12, 2013 at 1:45
  • 1
    @Hubris - Note the comment by Chris; it links to an answer that uses the same idea but is a lot fancier. It lets you pass a single string like "c.d" and then parses it for you, following the indicated path.
    – Ted Hopp
    Commented Aug 12, 2013 at 1:48
3

This is an old question and now with es6 features, this problem can be solved more easily.

const idx = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);

Thanks to @sharifsbeat for this solution.

3

The answers here are good bare-metal solutions. However, if you just want to use a package that is tried and true, I recommend using lodash.

With ES6 you can run the following

import _ from 'lodash'

var myDeepObject = {...}

value = _.get(myDeepObject, 'maybe.these.path.exist', 'Default value if not exists')

2

probably it's may be simple:

let a = { a1: 11, b1: 12, c1: { d1: 13, e1: { g1: 14 }}}
console.log((a || {}).a2); => undefined
console.log(((a || {}).c1 || {}).d1) => 13

and so on.

0
const getValue = (obj, property, defaultValue) => (
  property.split('.').reduce((item, key) => {
    if (item && typeof item === 'object' && key in item) {
      return item[key];
    }
    return defaultValue;
  }, obj)
)

const object = { 'a': { 'b': { 'c': 3 } } };

getValue(object, 'a.b.c'); // 3
getValue(object, 'a.b.x'); // undefined
getValue(object, 'a.b.x', 'default'); // 'default'
getValue(object, 'a.x.c'); // undefined
0

I will just paste the function that I use in almost all project as utility for this type of situation.

public static is(fn: Function, dv: any) {
    try {
        if (fn()) {
                return fn()
            } else {
                return dv
            }
        } catch (e) {
            return dv
        }
    }

So first argument is callback and second is the default value if it fails to extract the data due to some error.

I call it at all places as follows:

var a = is(()=> a.b.c, null);
0

// The code for the regex isn't great, 
// but it suffices for most use cases.

/**
 * Gets the value at `path` of `object`.
 * If the resolved value is `undefined`,
 * or the property does not exist (set param has: true),
 * the `defaultValue` is returned in its place.
 *
 * @param {Object} object The object to query.
 * @param {Array|string} path The path of the property to get.
 * @param {*} [def] The value returned for `undefined` resolved values.
 * @param {boolean} [has] Return property instead of default value if key exists.
 * @returns {*} Returns the resolved value.
 * @example
 *
 * var object = { 'a': [{ 'b': { 'c': 3 } }], b: {'c-[d.e]': 1}, c: { d: undefined, e: 0 } };
 *
 * dotGet(object, 'a[0].b.c');
 * // => 3
 * 
 * dotGet(object, ['a', '0', 'b', 'c']);
 * // => 3
 *
 * dotGet(object, ['b', 'c-[d.e]']);
 * // => 1
 *
 * dotGet(object, 'c.d', 'default value');
 * // => 'default value'
 *
 * dotGet(object, 'c.d', 'default value', true);
 * // => undefined
 *
 * dotGet(object, 'c.d.e', 'default value');
 * // => 'default value'
 *
 * dotGet(object, 'c.d.e', 'default value', true);
 * // => 'default value'
 *
 * dotGet(object, 'c.e') || 5; // non-true default value
 * // => 5 
 * 
 */
var dotGet = function (obj, path, def, has) {
    return (typeof path === 'string' ? path.split(/[\.\[\]\'\"]/) : path)
    .filter(function (p) { return 0 === p ? true : p; })
    .reduce(function (o, p) {
        return typeof o === 'object' ? ((
            has ? o.hasOwnProperty(p) : o[p] !== undefined
        ) ? o[p] : def) : def;
    }, obj);
}

1
  • 1
    Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
    – Community Bot
    Commented Jul 20, 2022 at 4:48
0

For node version >=14

You can use the optional chaining (?.) operator, like the . chaining operator, except that instead of causing an error if a reference is nullish (null or undefined), the expression short-circuits with a return value of undefined.

const sampleObj = {
    name: 'stackOverflow',
    dog: {
        name: 'test'
    }
};

const catName = sampleObj.cat?.name;
console.log(catName);
// Expected output: undefined

console.log(sampleObj.someNonExistentAPI?.());
// Expected output: undefined

And for node version < 14

You can use if-else with && operator

if (samleObj && sampleObj.cat && sampleObj.cat.name) {
    const catName = sampleObj.cat.name
}
0

A combination of Destructuring_assignment, Array & Object Spread syntax works well in my case:

let json = {a: 1, b: {c: 2, d: 3, e: 4}};

let traverse = ([prop, ...props], {[prop]: obj} = {}) => 
  props.length ? traverse(props, obj) : obj;

let w = traverse(['b', 'd'], json);       // 3
let x = traverse(['x', 'd'], json);       // undefined (parent prop x missing)
let y = traverse(['b', 'x'], json);       // undefined (child prop x missing)
let z = traverse(['b', 'd', 'x'], json);  // undefined (child prop x missing)
console.log(w);
console.log(x);
console.log(y);
console.log(z)

-1

If you would like to have a dynamic access with irregular number of properties at hand, in ES6 you might easily do as follows;

function getNestedValue(o,...a){
  var val = o;
  for (var prop of a) val = typeof val === "object" &&
                                   val !== null     &&
                                   val[prop] !== void 0 ? val[prop]
                                                        : undefined;
  return val;
}
var obj = {a:{foo:{bar:null}}};
console.log(getNestedValue(obj,"a","foo","bar"));
console.log(getNestedValue(obj,"a","hop","baz"));

-1

Gets the value at path of object. If the resolved value is undefined, the defaultValue is returned in its place.

In ES6 we can get nested property from an Object like below code snippet.

const myObject = {
    a: {
      b: {
        c: {
          d: 'test'
        }
      }
    },
    c: {
      d: 'Test 2'
    }
  },

  isObject = obj => obj && typeof obj === 'object',

  hasKey = (obj, key) => key in obj;



function nestedObj(obj, property, callback) {
  return property.split('.').reduce((item, key) => {
    if (isObject(item) && hasKey(item, key)) {
      return item[key];
    }
    return typeof callback != undefined ? callback : undefined;
  }, obj);
}

console.log(nestedObj(myObject, 'a.b.c.d')); //return test


console.log(nestedObj(myObject, 'a.b.c.d.e')); //return undefined


console.log(nestedObj(myObject, 'c.d')); //return Test 2


console.log(nestedObj(myObject, 'd.d', false)); //return false


console.log(nestedObj(myObject, 'a.b')); //return {"c": {"d": "test"}}

-1

An old question, and now days we have Typescript projects so often that this question seems irrelevant, but I got here searching for the same thing, so I made a simple function to do it. Your thoughts about not using try/catch is too strict for my taste, after all the seek for undefined.x will cause an error anyway. So with all that, this is my method.

function getSafe (obj, valuePath) {
    try { return eval("obj." + valuePath); } 
    catch (err) { return null; }
}

To use this we have to pass the object. I tried to avoid that, but there was not other way to get scope into it from another function (there is a whole bunch of questions about this in here). And a small test set to see what we get:

let outsideObject = {
    html: {
        pageOne: {
            pageTitle: 'Lorem Ipsum!'
        }
    }
};
function testme() {  
    let insideObject = { a: { b: 22 } };
    return {
        b: getSafe(insideObject, "a.b"),       // gives: 22
        e: getSafe(insideObject, "a.b.c.d.e"), // gives: null
        pageTitle: getSafe(outsideObject, "html.pageOne.pageTitle"),     // gives: Lorem Ipsum!
        notThere: getSafe(outsideObject, "html.pageOne.pageTitle.style") // gives: undefined
    }
}
testme(); 

UPDATE: Regarding the use of eval I think that eval is a tool to use carefully and not the devil itself. In this method, the user does not interfere with eval since it is the developer that is looking for a property by its name.

-1

If you care about syntax, here's a cleaner version of Hosar's answer:

function safeAccess(path, object) {
  if (object) {
    return path.reduce(
      (accumulator, currentValue) => (accumulator && accumulator[currentValue] ? accumulator[currentValue] : null),
      object,
    );
  } else {
    return null;
  }
}

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