Get by Path
Getting a nested property by path with Javascript
January 25, 2022 • 4 min read
Following on from deepClone we are going to create a method which will enable us to access and return a nested property.
The function will take two arguments, a path and a source object. The path will consist of an Array that can contain strings and numbers e.g. ['personalInfo', 'name', 0]. These will correspond to object properties and indexes in an Array.
Let's start with an example.
const person = {
id: 1,
personalInfo: {
name: ['Fred', 'Flinstone'],
address: {
road: '222 Rocky Way',
city: 'Bedrock 70777'
}
}
}
console.log(getByPath(['personalInfo', 'address', 'road'], person))
// '222 Rocky Way'
console.log(getByPath(['personalInfo', 'name', 1], person))
// 'Flinstone'
Using a for of loop
With deepClone recursion was the appropriate choice. Here we can just use a simple loop.
const getByPath = (keys, source) => {
let value = source
for (const key of keys) {
if (!hasOwn(value, key)) return undefined
value = value[key]
}
return deepClone(value)
}
A reference to the source object is assigned to value.
We then loop through the path keys. If the property in our path doesn't exist undefined is returned otherwise we re-assign value to the next property.
On successful completion of the loop and path a deepClone of the property is returned.
Optional Chaining (?.)
A modern approach to accessing nested properties is to use optional chaining. If a property in the chain isn't valid instead of throwing a type error it short-circuits returning undefined.
Here is an example where we try to access name on the undefined property info.
console.log(person.info.name)
// TypeError: Cannot read properties of undefined (reading 'name')
// Optional Chaining
console.log(person?.info?.name)
// undefined
This removes the need for explicitly checking if a property exists as we did with hasOwnProperty in the previous example.
const getByPath = (keys, source) => {
let value = source
for (const key of keys) value = value?.[key]
return deepClone(value)
}
Here is an alternative using Array.reduce
const getByPath = (keys, source) => (
deepClone(keys.reduce((prop, key) => prop?.[key], source))
)
A Taste of Currying
We will have a detailed look at currying in some future articles, but in the meantime I want to give you a quick taster.
The above getByPath may seem a bit pedestrian, after all why not just access the property manually with const name = person.personalInfo.name.
It is when we introduce currying — a process that enables us to fix arguments — that functions like getByPath come into their own.
To create a curried getByPath, we first need to pass it into a curry function. Again I will be covering how to write our own curry function in an upcoming article.
// getByPath passed into curry function returning a curried version
const getByPath = curry((keys, source) => {
let value = source
...
return deepClone(value)
})
For this exercise let's start with a simple array of objects.
const characters = [
{
id: 1,
personalInfo: {
name: ['Fred', 'Flinstone'],
address: {
road: '222 Rocky Way',
city: 'Bedrock 70777'
}
}
},
{
id: 2,
personalInfo: {
name: ['Barney', 'Rubble'],
address: {
road: '223 Rocky Way',
city: 'Bedrock 70777'
}
}
}
]
We will then use our curried getByPath to access the addresses in the characters collection.
// getByPath returns a function to addresses
const addresses = getByPath(['personalInfo', 'address'])
// addresses is passed directly into characters.map
console.log(characters.map(addresses))
/*
[
{ road: '222 Rocky Way', city: 'Bedrock 70777' },
{ road: '223 Rocky Way', city: 'Bedrock 70777' }
]
*/
What is noteworthy here is characters.map(addresses). The addresses argument passed into characters.map is a curried function returned by getByPath(['personalInfo', 'address']). It's path argument was set and stored, and is accessible to the returned addresses function.
Essentially we have split the calling of getByPath into a two stage process, which may seem convoluted but does have it's benefits. Now every time the addresses function is called due to it already having access to it's stored path ['personalInfo', 'address'], we only need to pass in a target object to get a result.
This makes it ideal for processing a sequence of objects.
Conclusion
We have looked at a few implementations of what is essentially a very simple function. I wanted to give you an example of currying to illustrate how we can take a simple function and turn it into something a little more unique.
In the following article we will be looking at getByPath's companion setByPath.