Enter Your Email Address to Watch This Lesson

Your link to unlock this lesson will be sent to this email address.

Unlock this lesson and all 986 of the free egghead.io lessons, plus get JavaScript content delivered directly to your inbox!



Existing egghead members will not see this. Sign in.

Get Deeply Nested Properties Safely with Ramda's path and pathOr Functions

4:03 JavaScript lesson by

In this lesson we'll see how Ramda's path and pathOr functions can be used to safely access a deeply nested property from an object while avoiding the dreaded checks for undefined at each new property in the desired path.

Get the Code Now
click to level up

egghead.io comment guidelines

Avatar
egghead.io

In this lesson we'll see how Ramda's path and pathOr functions can be used to safely access a deeply nested property from an object while avoiding the dreaded checks for undefined at each new property in the desired path.

Avatar
Marko

What if we have random number as key name? Like "1234567": { ... }
This happens often, for example that's how lists are composed in Firebase.

In reply to egghead.io
Avatar
Andrew Van Slaars

Marko,

I'm assuming you want to get a property in a path where you know which random key you're targeting. Like as part of an event handler's data or as a URL param or something similar. I'm envisioning a list of items from Firebase and you want to drill into the details of a particular item and you have that key available. With this assumption in mind, there are probably 10 different ways to solve this with Ramda, ranging from simple to unreadable... I'll go with simple and readable here:

Assuming this contrived data structure:

const data = {
  targets: {
    "random1234": {
      name: "1234",
      nested: {
        stuff: {
          and: {
            things: "Here it is"
          }
        }
      }
    },
    "random4321": {
      name: "4321"
    }
  }
}

You could create a function like so:

const getByKey = (key) => pathOr('default', ['targets', key, 'nested', 'stuff', 'and', 'things'])

Where you just put that key value in the appropriate place in the path array

Then you can call it like so:

const result = getByKey('random1234')(data)

Since the function returns another function, you can call it with the key ahead of time and pass in the data later, like in a composition, but if you want to pass the key and data together, you end up with the two sets of parens like the sample above. To allow the key and data to be passed in as two arguments to a single function, you can use uncurryN like so:

const getByKey = uncurryN(2, (key) => pathOr('default', ['targets', key, 'nested', 'stuff', 'and', 'things']))

const result = getByKey('random1234', data)

If you want to get more detail on the uncurryN function, there is a video for that here: https://egghead.io/lessons/javascript-curry-and-uncurry-functions-with-ramda

Hope this helps.

In reply to Marko
Avatar
Marko

Hi Andrew,

Wow, thanks! Yes this is applicable only when you know the "random1234" part.

I was thinking of a way to have part of path "optional", like: ['targets', '*', 'name', 'nested', 'stuff', 'and', 'things']

Otherwise we could use something similar to Object.keys, or generally somehow get to the random key to be used as function parameter like you wrote.

In reply to Andrew Van Slaars

Here I have two objects representing departments in a company. As you can see, the AccountingDepartment is much more complete with a personnel section which includes a manager, with a first name, last name, title, and salary. The ITDepartment just has the name and location. Our result is the last name of the manager under personnel for the AccountingDepartment, and if I run this, we'll see that I get the last name printed out.

If I change the AccountingDepartment to the ITDepartment, however, my output's going to go away, and if I open my console we can see that's because I have a type error, cannot read property manager of undefined. The problem here is that personnel is not defined in ITDepartment, so we get an exception.

In order to make this a safer operation and avoid this exception, what I could do is I could come in here and take ITDepartment.personnel. I could add a check for that, and if that's truthy, then it will evaluate the next part, so if my ITDepartment now has personnel, but it still doesn't have manager, we'll see I get a type exception again.

What I can do is I can come in here and then I can add another check for ITDepartment.personnel.manager, and another double ampersand, and I'll get back undefined. At least I'm not getting the exception anymore, but the problem here is if I want to be able to do the same thing with the AccountingDepartment, now I need to take all these checks and basically duplicate them for the accounting department. I could take this and I could wrap this all up in a function that would work on anything with this particular path, but there's a better way.

What I'm going to do, is I've included Ramda in this file, so I'm just going to use destructuring to pull in some functions. I'm going to start with path, and then on my result what I can do is actually call a function that I'm going to create using path, so we're going to call it getManagerLast.

I'm going to use Ramda's path, and path is going to take two arguments. It's going to take an array of property names, and those property names are going to be the properties that we are calling with our path here.

We're going to do personnel, followed by manager, followed by whole name. Now getManagerLast, because it's a curried function, is going to return a new function that's waiting for its second argument. In our case that second argument is going to be the object that we want to get that value from. I'm going to come down here and I'm going to call getManagerLast and I'm going to pass it ITDepartment, and I can get rid of the rest of this. We'll see that I get back undefined.

I didn't have to do all those checks. The property still doesn't exist in ITDepartment, but it's doing what it's supposed to do. If I switch this back to the AccountingDepartment, we'll see that we get the expected value. Now this operation is much safer. I've avoided the error, but I don't necessarily want to get undefined. From passing in the ITDepartment it's great that the exception doesn't get thrown anymore, but I want a value here.

What I can do is I can just use || to say or, and if it's not a truthy value I can just say nobody, and that works, or I could make this part of my getManagerLast function. What I can do is I can take this out of here, get rid of those double pipes, and I can pull in another function from Ramda called pathor. Pathor is going to take my array as a path, but before that, it's going to take a default value. I'll pass in my nobody string, followed by my array.

I'll get back a function I can pass an object to, and it will either get the item on the path, like the AccountingDepartment where I get my value, or it will get undefined and replace it with that default value.

HEY, QUICK QUESTION!
Joel's Head
Why are we asking?