<- johnlindquist.com

Safely Get Nested Values with Proxies

We've all been in the situation where we are trying to access a very deeply nested property inside of an object but it errors out because one of those properties you're trying to access is undefined.

let person = {}
person.name.first
//Uncaught TypeError: Cannot read property 'first' of undefined

Strings to the Rescue!

Many of us turn to a tool called lodash which has a get method just safely access properties by using strings and if the property isn't there you can set a default for it to return:

import { get } from 'lodash'
let person = {}
let first = get(person, 'name.first')
console.log(first) //logs "undefined"

But I Don't Like Strings...

We can flip the script by using a Proxy to hijack all of the get calls to allow us to attempt to access the property using "dots" as were used to rather than using strings:

let person = {}
let handler = {
get() {
return new Proxy({}, handler)
},
}
person = new Proxy(person, handler)
console.log(person.name.first)
//logs `Proxy{}`, but no Errors about an undefined "name"!

Unfortunately, with this approach, we've broken basic access to any properties on the object even if they do exist:

let person = {
name: {
first: 'John',
},
}
let handler = {
get() {
return new Proxy({}, handler)
},
}
person = new Proxy(person, handler)
console.log(person.name.first)
//logs `Proxy{}` even though "first" is defined :(

So let's go ahead and add in the happy path in our handler to get access to the property we're looking for by checking the target and prop:

let handler = {
get(target, prop) {
if (target[prop]) {
return target[prop]
}
return new Proxy({}, handler)
},
}

So now our code will happily log out appropriate values and ignore any undefined values with a Proxy:

let person = {
name: {
first: 'John',
},
}
let handler = {
get(target, prop) {
if (target[prop]) {
return target[prop]
}
return new Proxy({}, handler)
},
}
person = new Proxy(person, handler)
console.log(person.name.first) //logs "John"
console.log(person.contact.email.provider) //logs `Proxy{}`

But... we can break this easily by adding a contact object to our person:

let person = {
name: {
first: 'John',
},
contact: {},
}
//...
console.log(person.contact.email.provider)
//Uncaught TypeError: Cannot read property 'provider' of undefined

Our happy path scenario didn't cover when you start on a happy path, but then break out into a sad path. So let's finish off by covering when a nested object has undefined values:

let handler = {
get(target, prop) {
let value = target[prop]
if (value) {
if (typeof value === 'object') {
return new Proxy(value, handler)
}
return value
}
return new Proxy({}, handler)
},
}
//...
console.log(person.name.first) //logs "John"
console.log(person.contact.email.provider) //logs `new Proxy`

Now we're back to our error-free approach and we can even add in some neat debugging features!

let handler = {
get(target, prop) {
let value = target[prop]
console.log(`${prop} is ${value}`) //Log access to this prop
if (value) {
if (typeof value === 'object') {
return new Proxy(value, handler)
}
return value
}
return new Proxy({}, handler)
},
}
// --- Logs ---
// name is [object Object]
// first is John
// contact is [object Object]
// email is undefined
// provider is undefined

A Flawed Solution, But There's More...

I'll be the first to admit getting a value of Proxy when you're supposed to get an undefined will probably screw up a few things in your app. But this does open the door for us to make some really interesting tools with Proxy while keeping a slick API that avoids strings. Stay tuned for more... 😉

Prove It!

Think you understand how to use the handler.get API? Write a handler.get that always returns "John" when accessing any index of an Array: