Categories
javascript nested object properties

Test for existence of nested JavaScript object key

841

If I have a reference to an object:

var test = {};

that will potentially (but not immediately) have nested objects, something like:

{level1: {level2: {level3: "level3"}}};

What is the best way to check for the existence of property in deeply nested objects?

alert(test.level1); yields undefined, but alert(test.level1.level2.level3); fails.

I’m currently doing something like this:

if(test.level1 && test.level1.level2 && test.level1.level2.level3) {
    alert(test.level1.level2.level3);
}

but I was wondering if there’s a better way.

9

687

+500

You have to do it step by step if you don’t want a TypeError because if one of the members is null or undefined, and you try to access a member, an exception will be thrown.

You can either simply catch the exception, or make a function to test the existence of multiple levels, something like this:

function checkNested(obj /*, level1, level2, ... levelN*/) {
  var args = Array.prototype.slice.call(arguments, 1);

  for (var i = 0; i < args.length; i++) {
    if (!obj || !obj.hasOwnProperty(args[i])) {
      return false;
    }
    obj = obj[args[i]];
  }
  return true;
}

var test = {level1:{level2:{level3:'level3'}} };

checkNested(test, 'level1', 'level2', 'level3'); // true
checkNested(test, 'level1', 'level2', 'foo'); // false

ES6 UPDATE:

Here is a shorter version of the original function, using ES6 features and recursion (it’s also in proper tail call form):

function checkNested(obj, level,  ...rest) {
  if (obj === undefined) return false
  if (rest.length == 0 && obj.hasOwnProperty(level)) return true
  return checkNested(obj[level], ...rest)
}

However, if you want to get the value of a nested property and not only check its existence, here is a simple one-line function:

function getNested(obj, ...args) {
  return args.reduce((obj, level) => obj && obj[level], obj)
}

const test = { level1:{ level2:{ level3:'level3'} } };
console.log(getNested(test, 'level1', 'level2', 'level3')); // 'level3'
console.log(getNested(test, 'level1', 'level2', 'level3', 'length')); // 6
console.log(getNested(test, 'level1', 'level2', 'foo')); // undefined
console.log(getNested(test, 'a', 'b')); // undefined

The above function allows you to get the value of nested properties, otherwise will return undefined.

UPDATE 2019-10-17:

The optional chaining proposal reached Stage 3 on the ECMAScript committee process, this will allow you to safely access deeply nested properties, by using the token ?., the new optional chaining operator:

const value = obj?.level1?.level2?.level3 

If any of the levels accessed is null or undefined the expression will resolve to undefined by itself.

The proposal also allows you to handle method calls safely:

obj?.level1?.method();

The above expression will produce undefined if obj, obj.level1, or obj.level1.method are null or undefined, otherwise it will call the function.

You can start playing with this feature with Babel using the optional chaining plugin.

Since Babel 7.8.0, ES2020 is supported by default

Check this example on the Babel REPL.

🎉🎉UPDATE: December 2019 🎉🎉

The optional chaining proposal finally reached Stage 4 in the December 2019 meeting of the TC39 committee. This means this feature will be part of the ECMAScript 2020 Standard.

21

  • 4

    arguments is not actually an array. Array.prototype.slice.call(arguments) converts it to a formal array. Learn

    – deefour

    Nov 10, 2012 at 1:00


  • 24

    this’d be a lot more efficient to do var obj = arguments[0]; and start from var i = 1 instead of copying the arguments object

    – Claudiu

    Oct 31, 2013 at 19:45


  • 2

    I put together a version with try/catch for austerity sake, and no surprise – performance is awful (except in Safari for some reason). There are some answers below that are pretty performant, along with Claudiu’s modification which is also significantly more performant than selected answer. See jsperf here jsperf.com/check-if-deep-property-exists-with-willnotthrow

    Nov 27, 2014 at 5:35

  • 3

    In ES6 the args variable declaration can be removed and and ...args can be used as the second argument for the checkNested method. developer.mozilla.org/en/docs/Web/JavaScript/Reference/…

    – Vernon

    Dec 28, 2016 at 15:49

  • 9

    This is a very unmaintainable. If any property keys change (they will), all devs on the project would have to ‘string search’ the entire codebase. This isn’t really a solution to the problem, as it introduces a much bigger problem

    – Drenai

    Jan 14, 2018 at 11:20


377

Here is a pattern I picked up from Oliver Steele:

var level3 = (((test || {}).level1 || {}).level2 || {}).level3;
alert( level3 );

In fact that whole article is a discussion of how you can do this in javascript. He settles on using the above syntax (which isn’t that hard to read once you get used to it) as an idiom.

12

  • 8

    @wared I think it is interesting mostly for how concise it is. There is a detailed discussion of the performance characteristics in the linked post. Yes it always does all the tests, but it avoids creating temp vars, and you can alias {} to a var if you want to prevent the overhead of creating a new empty object each time. In 99% of cases I would not expect speed to matter, and in cases where it does there is no substitute for profiling.

    Sep 5, 2013 at 18:13


  • 9

    @MuhammadUmer No, the point of (test || {}) is that if test is undefined, then you’re doing ({}.level1 || {}). Of course, {}.level1 is undefined, so that means you’re doing {}.level2, and so on.

    Apr 28, 2015 at 16:06

  • 3

    @JoshuaTaylor: I think he means if test is not declared, there’ll be a ReferenceError, but that’s not a problem, because if it’s not declared, there’s a bug to be fixed, so the error is a good thing.

    – user1106925

    Jan 2, 2016 at 16:15

  • 41

    you said “which isn’t that hard to read once you get used to it”. Well, these are signs you know already this is a mess. Then why suggest this solution? It is prone to typos and gives absolutely nothing to readability. Just look it! If i have to write an ugly line, it should asswell be readable; so i’m going to just stick with if(test.level1 && test.level1.level2 && test.level1.level2.level3)

    – Sharky

    Sep 30, 2016 at 9:16


  • 11

    Unless I’m missing something, this won’t work for boolean end-properties that might be false… sadly. Otherwise I love this idiom.

    – T3db0t

    Nov 17, 2016 at 20:01

285

Update

Looks like lodash has added _.get for all your nested property getting needs.

_.get(countries, 'greece.sparta.playwright')

https://lodash.com/docs#get


Previous answer

lodash users may enjoy lodash.contrib which has a couple methods that mitigate this problem.

getPath

Signature: _.getPath(obj:Object, ks:String|Array)

Gets the value at any depth in a nested object based on the path described by
the keys given. Keys may be given as an array or as a dot-separated string.
Returns undefined if the path cannot be reached.

var countries = {
        greece: {
            athens: {
                playwright:  "Sophocles"
            }
        }
    }
};

_.getPath(countries, "greece.athens.playwright");
// => "Sophocles"

_.getPath(countries, "greece.sparta.playwright");
// => undefined

_.getPath(countries, ["greece", "athens", "playwright"]);
// => "Sophocles"

_.getPath(countries, ["greece", "sparta", "playwright"]);
// => undefined

7

  • Lodash really needs a _.isPathDefined(obj, pathString) method.

    Jan 9, 2015 at 16:34

  • @MatthewPayne It’d be nice perhaps, but it really isn’t necessary. You could do it yourself really easily function isPathDefined(object, path) { return typeof _.getPath(object, path) !== 'undefined'; }

    – Thor84no

    May 12, 2015 at 11:33


  • 12

    Lodash has this same functionality itself: _.get(countries, 'greece.sparta.playwright', 'default'); // → 'default' _.has(countries, 'greece.spart.playwright') // → false

    – Tom

    May 15, 2015 at 18:23


  • even better would be _.result

    Aug 29, 2015 at 8:50

  • If you need to determine multiple different paths consider: var url = _.get(e, 'currentTarget.myurl', null) || _.get(e, 'currentTarget.attributes.myurl.nodeValue', null) || null

    May 4, 2017 at 4:37