Safely Accessing Deeply Nested Values In JavaScript

Many Ways To Safely Access Nested Data.

Intro

This is a short post intended to show the many different ways on how to safely access deeply nested values in JavaScript. The following examples all do the same thing and while they may vary in approach they all solve the same problem.

Before we start. Let’s see in more detail what we’re actually trying to achieve here.

const props = {
user: {
posts: [
{ title: 'Foo', comments: [ 'Good one!', 'Interesting...' ] },
{ title: 'Bar', comments: [ 'Ok' ] },
{ title: 'Baz', comments: [] },
]
}
}

Image we have a props objects, which might look something like the above example. Now say, we wanted to get hold of the comments on the user’s first posting. How would we do this with regular JavaScript?

// access deeply nested values...
props.user &&
props.user.posts &&
props.user.posts[0] &&
props.user.posts[0].comments

This might look like something we might come across and it’s sensible to the point, that we want to make sure a key or index exists before we try to access it. To think this further through, let’s assume we also wanted to only read the first comment. To solve this we might update our previous example to also check if comments actually exist, before accessing the first item.

// updating the previous example...
props.user &&
props.user.posts &&
props.user.posts[0] &&
props.user.posts[0].comments &&
props.user.posts[0].comments[0]

So every time we want to access any deeply nested data we have to explicitly do a manual check. To clarify why this is troublesome, just imagine we didn’t want to check the users posts, but instead wanted to know the blog title of the user’s last comment. We would not be able to build on the previous example.

// accessing user's comments
props.user &&
props.user.comments &&
props.user.comments[0] &&
props.user.comments[0].blog.title

The example might be exaggerated, but you get the idea. We need to check the whole structure level for level until we reach the value we’re searching for.

Ok, so now that we have a better understanding of what problem we’re actually trying to solve, let’s take a look at the different ways we can approach this. The examples start by using JavaScript only, then Ramda and then Ramda with Folktale. While you might not need the more advanced examples, it’s should be interesting none the less. Especially considering what we gain by taking a safe approach when accessing deeply nested values.


Barebones JavaScript

To start things off, we don’t want to really have to manually check for nullable or undefined, instead we could rollout our own small but compact function and be flexible with any input data provided.

const get = (p, o) =>
p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o)
// let's pass in our props object...
console.log(get(['user', 'posts', 0, 'comments'], props))
// [ 'Good one!', 'Interesting...' ]
console.log(get(['user', 'post', 0, 'comments'], props))
// null

Let’s breakdown our get function.

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

We’re passing in a path definition as the first argument and the object, we want to retrieve the values from, as the second.

Regarding the fact, that the second argument is the object, you might be asking yourself: what do we gain from this? The ability to define a common function that knows a specific path and expects any object that might or might not have the given path.

const getUserComments = get(['user', 'posts', 0, 'comments'])

By choosing this approach we can call getUserComments with our previous props object or with any another object if we like. It would also imply that we have to curry our get function like so.

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

Finally we can log the result and verify if everything is working as expected.

console.log(getUserComments(props))
// [ 'Good one!', 'Interesting...' ]
console.log(getUserComments({user:{posts: []}}))
// null

Our get function is in essence a reduce over the provided path.

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

Let’s take a simplified path, where we only want to access the id.

['id'].reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, {id: 10})

We initialize the reduce function with the provided object and then check if the object is defined and if yes, verify if the key exists. Depending on the result of (xs && xs[x]) we either return the value or null and so on.

As shown, we can easily mitigate the problem having to implicitly check for any nullable or undefined values. If you would rather pass in a string path instead of an array, the get function will only need some minor adaptions, which I’ll leave for the interested reader to implement.


Ramda

Instead of writing our own function, we can also leverage Ramda functions to achieve the same.

One function that comes out of the box with Ramda is path. path expects two arguments, the path and the object. Let’s rewrite the example in Ramda.

const getUserComments = R.path(['user', 'posts', 0, 'comments'])

Now we can call getUserComments with props, either get back the needed value or null if nothing is found.

getUserComments(props) // [ 'Good one!', 'Interesting...' ]
getUserComments({}) // null

But what if we wanted to return something different than null in case we can’t find the specified path? Ramda also offers pathOr. pathOr expects a default value as the initial argument.

const getUserComments = R.pathOr([], ['user', 'posts', 0, 'comments'])
getUserComments(props) // [ 'Good one!', 'Interesting...' ]
getUserComments({}) // []

Thanks to Gleb Bahmutov for providing insights on path and pathOr.


Ramda + Folktale

Let’s also add Folktale’s Maybe into the mix. For example we could build a generic getPath function that expects a path as well as the object we want to retrieve the values from.

const getPath = R.compose(Maybe.fromNullable, R.path)
const userComments =
getPath(['user', 'posts', 0, 'comments'], props)

Calling getPath will either return a Maybe.Just or a Maybe.Nothing.

console.log(userComments) // Just([ 'Good one!', 'Interesting...' ])

What do we gain by wrapping our result in a Maybe? By taking this approach we can now safely continue to work with userComments without ever having to manually check if userComments returned a null or the wanted value.

console.log(userComments.map(x => x.join(',')))
// Just('Good one!,Interesting...')

The same works when no value is found.

const userComments =
getPath(['user', 'posts', 8, 'title'], props)

console.log(userComments.map(x => x.join(',')).toString())
// Nothing

Check the example.

We can also wrap all our props inside Maybe. This enables us to use composeK, which knows how to chain the props together.

// example using composeK to access a deeply nested value.
const getProp = R.curry((name, obj) =>
Maybe.fromNullable(R.prop(name, obj)))
const findUserComments = R.composeK(
getProp('comments'),
getProp(0),
getProp('posts'),
getProp('user')
)
console.log(findUserComments(props).toString())
// Just([ 'Good one!', 'Interesting...' ])
console.log(findUserComments({}).toString())
// Nothing

Check the example.

This is all very advanced and using Ramda’s path should suffice. But let’s just take a quick look at other examples. f.e. we could also use Ramda’s compose and chain to achieve the same as the above example.

// using compose and chain
const getProp = R.curry((name, obj) =>
Maybe.fromNullable(R.prop(name, obj)))
const findUserComments =
R.compose(
R.chain(getProp('comments')),
R.chain(getProp(0)),
R.chain(getProp('posts')),
getProp('user')
)
console.log(findUserComments(props).toString())
// Just([ 'Good one!', 'Interesting...' ])
console.log(findUserComments({}).toString())
// Nothing

Check the example.

The same as the previous examples but this time using pipeK.

// example using pipeK to access a deeply nested value.
const getProp = R.curry((name, obj) =>
Maybe.fromNullable(R.prop(name, obj)))
const findUserComments = R.pipeK(
getProp('user'),
getProp('posts'),
getProp(0),
getProp('comments')
)
console.log(findUserComments(props).toString())
// Just([ 'Good one!', 'Interesting...' ])
console.log(findUserComments({}).toString())
// Nothing

Check the example.

Also check the pipeK example using map as a bonus. Thanks to Tom Harding for providing the pipeK examples.


Lenses

Finally we can also use lenses. Ramda comes with lensProp and lensPath.

// lenses
const findUserComments =
R.lensPath(['user', 'posts', 0, 'comments'])
console.log(R.view(findUserComments, props))
// [ 'Good one!', 'Interesting...' ]

Again, we can wrap the result inside a Maybe just like we have done with our path example. Lenses are very useful when needing to update any nested values. For more information on lenses also check my An Introduction Into Lenses In JavaScript post.

Summary

We should have a good overview of the many ways we can retrieve values when dealing with nested data. Besides knowing how to rollout our own implementation, we should also have a basic understanding of the functions Ramda offers regarding the matter and maybe even have a better understanding why wrapping our results inside an Either or Maybe makes sense. Further more we touched the lenses topic, which besides enabling us to retrieve any values also enables us to update deeply nested data without mutating our object.

Finally, you should not need to write any code like the following from here on out.

// updating the previous example...
props.user &&
props.user.posts &&
props.user.posts[0] &&
props.user.posts[0].comments &&
props.user.posts[0].comments[0]

Special thanks to Gleb Bahmutov, Tom Harding and Toastal for providing examples and insights on Twitter.

Update: 24.03.2017

Gleb Bahmutov published Call me Maybe which takes the discussed concepts here, even a step further. Highly recommended read.

If you have any feedback please leave a comment here or on Twitter.