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" : {}
},
]
}
Let's try accessing some values
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.
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'.
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'
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 (?.) operator
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
get(object, path, [defaultValue])
First, we will need to install lodash
npm install lodash
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
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
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
Safely setting values for deeply nested objects
Using Lodash
We can use Lodash's set function. Below is the synax
set(object, path, value)
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
]
}
}
*/
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
]
}
}
*/
assocPath is not an in-place function, i.e it does not update the object. It returns a new object.