DEV Community

Cover image for How to safely work with nested objects in JavaScript
Rahul Banerjee
Rahul Banerjee

Posted on • Originally published at realpythonproject.com

How to safely work with nested objects in JavaScript

Why do we need to be careful when working with nested objects?

If you have worked with APIs before, you have most likely work with deeply nested objects.
Consider the following object

const someObject = {
    "type" : "Objects",
    "data": [
        {
            "id" : "1",
            "name" : "Object 1",
            "type" : "Object",
            "attributes" : {
                "color" : "red",
                "size" : "big",
                "arr": [1,2,3,4,5]
            },
        },
        {
            "id" : "2",
            "name" : "Object 2",
            "type" : "Object",
            "attributes" : {}
        },
    ]
}
Enter fullscreen mode Exit fullscreen mode

Let's try accessing some values

console.log(
    someObject.data[0].attributes.color
)
// red
Enter fullscreen mode Exit fullscreen mode

This works fine but what if we try to access the 'color' property of the second element in data.

console.log(
    someObject.data[1].attributes.color
)
// undefined
Enter fullscreen mode Exit fullscreen mode

It prints undefined because the property 'aatributes' is empty. Let's try accessing second element inside the property 'arr'.

console.log(
    someObject.data[0].attributes.arr[1]
)
// 2


console.log(
    someObject.data[1].attributes.arr[1]
)
// TypeError: Cannot read property '1' of 
// undefined
Enter fullscreen mode Exit fullscreen mode

In the first case, 2 is printed in the console. However in the second case we get an error.

This is because 'someObject.data[1].attributes' is empty and therefore 'attributes.arr' is undefined. When we try accessing 'arr[1]', we are actually trying to index undefined which causes an error.

We could put the code inside a try..catch block to handle the error gracefully but if you have quite a few cases where you need to access deeply nested values, your code will look verbose.

Let's look at another scenario. This time we want to update the value of the element at index 0 in 'arr'

someObject.data[0].attributes.arr[0] = 200;
console.log(someObject.data[0].attributes.arr);
// [ 200, 2, 3, 4, 5 ]

someObject.data[1].attributes.arr[0] = 300;
// TypeError: Cannot set property '0' of 
// undefined
Enter fullscreen mode Exit fullscreen mode

We get a similar Type Error again.

Safely accessing deeply nested values

Using Vanilla JS

We can use the Optional chaining (?.) operator

console.log(
    someObject?.data[1]?.attributes?.color
)
// undefined

console.log(
    someObject?.data?.[1]?.attributes?.arr?.[0]
)
// undefined
Enter fullscreen mode Exit fullscreen mode

Notice this time it doesn't cause an error, instead it prints undefined. The ?. causes the expression to short-circuit, i.e if the data to the left of ?. is undefined or null, it returns undefined and doesn't evaluate the expression further.

Using Lodash

If you do not want to see a bunch of question marks in your code, you can use Lodash's get function. Below is the syntax

get(object, path, [defaultValue])
Enter fullscreen mode Exit fullscreen mode

First, we will need to install lodash

npm install lodash
Enter fullscreen mode Exit fullscreen mode

Below is code snippet which uses the get function

const _ = require('lodash');

console.log(
    _.get(someObject,
   'data[1].attributes.color', 
   'not found')
)
// not found

console.log(
    _.get(someObject,
    'data[1].attributes.arr[0]')
)
// undefined
Enter fullscreen mode Exit fullscreen mode

The default value is optional, if you do not specify the default value, it will simply return undefined.

Using Rambda

We can either use the 'path' function or the 'pathOr' function. The difference is that with the 'pathOr' function, we can specify a default value.

To install Rambda

npm install rambda
Enter fullscreen mode Exit fullscreen mode

Below is the code snippet to access the values

console.log(
  R.pathOr(
      ["data", 1, "attributes", "color"], 
      someObject, 
      "not found")
);
// not found

console.log(
    R.path(
        ["data", 1, "attributes", "arr", 0], 
        someObject
        )
);
// undefined
Enter fullscreen mode Exit fullscreen mode

Safely setting values for deeply nested objects

Using Lodash

We can use Lodash's set function. Below is the synax

set(object, path, value)
Enter fullscreen mode Exit fullscreen mode

If we provide a path that doesn't exist, it will create the path.

const _ = require("lodash");

_.set(
    someObject
    ,"data[1].attributes.arr[1]"
    , 200
);

console.log(
    _.get(
        someObject,
        'data[1]'
    )
)

/*
{
  id: '2',
  name: 'Object 2',
  type: 'Object',
  attributes: { 
      arr: [ 
          <1 empty item>, 
          200 
        ] 
}
}
*/
Enter fullscreen mode Exit fullscreen mode

Initially the property 'attributes' was empty but when tried setting a value for 'attributes.arr[1]', a property 'arr' was added to 'attributes' and then an empty element was added and then 200 was added.

Basically if the path we specify doesn't exist, it will create that path and set the value.

Using Rambda

We can do something similar to Lodash's set function using assocPath function in Rambda.

const R = require("ramda");

const newObj = 
     R.assocPath(
        ['data','1','attributes','arr',1]
        ,200
        ,someObject
    )
console.log(
    R.path(['data','1'],newObj)
)

/*
{
  id: '2',
  name: 'Object 2',
  type: 'Object',
  attributes: { 
      arr: [ 
          <1 empty item>, 
          200 
        ] 
}
}
*/
Enter fullscreen mode Exit fullscreen mode

assocPath is not an in-place function, i.e it does not update the object. It returns a new object.

Top comments (0)