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.