How to safely work with nested objects in JavaScript
Tags: javascript, codenewbie, programming, computerscience
Published: 2021-07-18 05:38:48

### 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
```javascript
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" : {}
        },
    ]
}
```
Let's try accessing some values
```javascript
console.log(
    someObject.data[0].attributes.color
)
// red
```
This works fine but what if we try to access the 'color' property of the second element in data.
```javascript
console.log(
    someObject.data[1].attributes.color
)
// undefined
```
It prints undefined because the property 'aatributes' is empty. Let's try accessing second element inside the property 'arr'.

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


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

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'**

```javascript
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
```
We get a similar Type Error again.

### Safely accessing deeply nested values
#### Using Vanilla JS
We can use the [Optional chaining (?.)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) operator
```javascript
console.log(
    someObject?.data[1]?.attributes?.color
)
// undefined

console.log(
    someObject?.data?.[1]?.attributes?.arr?.[0]
)
// undefined
```
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
```javascript
get(object, path, [defaultValue])
```

First, we will need to install lodash
```bash
npm install lodash
```

Below is code snippet which uses the get function
```javascript
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
```
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
```bash
npm install rambda
```
Below is the code snippet to access the values

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

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

### Safely setting values for deeply nested objects
#### Using Lodash
We can use Lodash's set function. Below is the synax
```javascript
set(object, path, value)
```
If we provide a path that doesn't exist, it will create the path. 
```javascript
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 
        ] 
}
}
*/
```
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.

```javascript
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 
        ] 
}
}
*/
```

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