Object destructuring best practice in Javascript

Crunch Tech
Feb 11 · 12 min read

A summary of the evolution of object property access patterns at Crunch.

Photo by Victoria Alexander on Unsplash

At Crunch, one of the principles of our Engineering Manifesto is ‘build the right thing in the right way’. Among other things, this means that we put a strong emphasis on simplifying and standardising our tech stack, as well as promoting the use of code libraries and design patterns.

As part of that effort, the developers across our teams are encouraged to review the patterns we use within our codebase. If we find what seems to be a better way of doing something, after some discussion we agree on whether we should adopt that pattern for new or refactored work, to be used in place of, or along with what we already have in our toolbox. The move from ES5 to React/Redux/ES6+ a few years ago involved many of these decisions, one of which was to use ES6 destructuring of objects and arrays.

As we improve our knowledge, we adapt and update our destructuring patterns to something that our teams find easier to work with than what had previously been in place. This blog post is a short summary of where we started from with our object property access patterns, how we initially started to incorporate destructuring patterns, and how we continue to modify and evolve these patterns as our understanding grows.

What is Object Destructuring?

The destructuring assignment syntax is a JavaScript expression that makes it possible to unpack values from arrays, or properties from objects, into distinct variables. (MDN)

Object properties can be accessed by using the dot notation or the bracket notation:

const myObj = {
foo: 'bar',
age: 42
}
myObj.foo // 'bar' accessed through dot notation
myObj['age'] // 42 accessed through bracket notation

The object destructuring assignment syntax gives us a third way to access object properties:

const { foo } = myObj // 'bar'
const { age } = myObj // 42
// or all in one line:const { foo, age } = myObj // foo === 'bar', age === 42

Object Destructuring

Ok. First things first. Before ES6 object destructuring was even a thing, in addition to regular dot notation or bracket notation for object property access, we made great use of a utility function that we affectionately called getPropValue

It allowed us to pass in an object and a key, and return the value of that key from the object:

export const getPropValue = (obj, key) =>
key.split('.').reduce((o, x) =>
o == undefined ? o : o[x]
, obj)

(Note that the double equals ==checks for both undefined and null )

We could pass in a key of the form prop.nestedprop.nestedprop and it would happily return the value for that nested property.

For example, if the object were:

const obj = {
main: {
content: {
title: 'old pier',
description: 'seagulls paradise'
}
}
}

we could retrieve the title as:

const title = getPropValue(obj, 'main.content.title')
// 'old pier'

Very easy to use and well loved by all of our developers. One of the nice features of getPropValue is that it handled the case where some nested property is undefined:

const obj = {
main: undefined
}
}
const title = getPropValue(obj, 'main.content.title')
// undefined

or null:

const obj = {
main: null
}
}
const title = getPropValue(obj, 'main.content.title')
// null

First level object destructuring

As much as we loved getPropValue when ES6 object destructuring wandered into our office one day, all shiny and new whilst smelling of apples, somehow we knew, without saying it out loud, that things were going to change in our codebase. At first, we limited destructuring to one level.

For example:

const obj = {
main: 'Brighton seagull'
}
const { main } = obj || {}
// 'Brighton seagull'

Also, we quickly learned to add the || {}because destructuring fails when attempting to destructure from undefined or null as we can see in the example below:

// If a is undefined...> let a = undefined
undefined
> const { notAProperty } = a
TypeError: Cannot destructure property `notAProperty` of 'undefined' or 'null'.
// but if we set a guard of an empty object...> a = undefined || {}
{}
> const { alsoNotAProperty } = a
undefined
// Since alsoNotAProperty doesn't exist in the empty object {}, when we attempt to destructure, alsoNotAProperty gets set to undefined

Nested destructuring

After all teams got up to speed and comfortable with this initial one-level form of the object destructuring pattern, we collectively took it up a level to nested destructuring. We were still using getPropValue at this stage. Probably not as much as previously though. It was almost to the extent that getPropValue had an idea that something was up. We were not quite there yet. But the smell of apples was quite strong. What were we to do?


To destructure a nested property, we write it in a similar way to the shape of the object from which we are destructuring.

For example, to set title to be a const with value equal to the nested title property within the obj :

const obj = {
main: {
content: {
title: 'old pier',
description: 'a structure where once many people used to wander around leisurely. Now even the seagulls fly away.'
}
}
}
const { main: { content: { title } } } = obj || {}

So main is within obj , content is within main , and finally title is destructured from content …. Phew. Note that we get a value for just the final step of destructuring, meaning the value we destructure for title is assigned to the variable named title. We don’t assign main or content to variables.

This all works fine as long as those properties all exist in the obj and none of them are undefined or null.

When that’s not the case, we need to find a way to prevent errors such as the dreaded TypeError: Cannot destructure property ‘propertyName' of ‘undefined' or ‘null' error.

More on how to deal with null later.

First, let’s take a look at how to deal with undefined. We can deal with undefined through the use of defaults.

Setting defaults

If we want main to have a default value to guard against undefined, there are a few ways we can do that:

  1. Inline object defaults for each property:

We build it up, working from the outside in:

// First is 'main'. We can set a default of {} for 'main'
const { main = {} } = obj || {}
// Next is 'content'. We can set a default of {} for 'content'
const { main: { content = {} } = {} } = obj || {}
// Finally we have 'title'. We can set a default of 'defaultTitle' for 'title'
const { main: { content: { title = 'defaultTitle' } = {} } = {} } = obj || {}

Note: The above example shows how to set a default value for the title property. We don’t ever set a default title with the value ‘defaultTitle’ in our codebase. The example is to demonstrate the concept, not to show actual usage.

This pattern is useful for when obj does exist, but doesn’t contain the nested property that we want to destructure from it. Setting defaults at each level as in the example above allows us to deal with this case.

2. Inline default object for the main variable:

Here again we attempt to destructure main from obj . If obj is undefined or null , then main is destructured from {} . This would cause it to be undefined (since it doesn’t exist in {} ) but since we have set a default of defaultMain , main ends up being set to this. And since content and title exist in defaultMain we can destructure them out without having to set inline empty object defaults.

const defaultMain = {
content: {
title: 'defaultTitle',
description: 'defaultDescription'
}
}
const { main: { content: { title } } = defaultMain } = obj || {}

This pattern is useful when obj doesn’t exist. There’s a gotcha where obj does exist but doesn’t contain all the properties necessary for the destructuring to work. (See the section below on Gotchas for more.)

3. Inline default object on the right:
Similar to case 2 above, but instead of having a guard against undefined with an empty object, and setting a default of defaultMain for main , this time we put a literal default object in place of the || {}

const { main: { content: { title } } } 
= obj || { main: { content: { title: 'defaultTitle' } } }

This has the same gotcha where obj does exist but doesn’t contain all the properties necessary for the destructuring to work. (See the section below on Gotchas for more.)

4. Default object variable on the right:
Similar to case 3, but defining defaultObj outside of our destructuring statement.

const defaultsMain = {
content: {
title: 'defaultTitle',
description: 'defaultDescription'
}
}
const defaultObj = { main: defaultsMain }
const { main: { content: { title } } } = obj || defaultObj

Once again, there’s the gotcha where obj does exist but doesn’t contain all the properties necessary for the destructuring to work. (See the section below on Gotchas for more.)

Which nested object destructuring pattern is best?

We found pattern 1 useful for one or two levels of nested properties. Anything more than that and it gets difficult to read and to follow which default is being set for which property. However, it does prevent the gotchas mentioned earlier.

We used pattern 2 for a short while before quickly moving on to patterns 3 and 4.

We found pattern 3 useful for one or two levels of nested properties.

Pattern 4 helps to make things easier to read and reason about, especially for larger objects and when destructuring multiple properties at the same time.

As an example of destructuring multiple properties, here we destructure title and description from obj , with a guard of defaultObj if obj doesn’t exist:

const defaultsMain = {
content: {
title: 'defaultTitle',
description: 'defaultDescription'
}
}
const defaultObj = { main: defaultsMain }
const { main: { content: { title, description } } } = obj || defaultObj

Gotchas

One thing to be wary of is the case where obj does exist, but doesn’t contain the property we want to destructure. Patterns 2, 3 and 4 will fail for this case, even if the default we supply does have that property. Since obj does exist, we don’t ever get to use the default.

const obj = {
main: {
content: {
title: 'old pier',
description: 'even the seagulls fly away.'
}
}
}
const defaultMain = {
content: {
title: 'defaultTitle',
description: 'defaultDescription',
copy: [
'some copy text',
'some more copy text'
]
}
}
const defaultObj = { main: defaultMain }// -------------------// this works (if we destructure from defaultObj)const { main: { content: { copy: [ firstLineOfCopy ] } } } = defaultObj// 'some copy text'// -------------------// this doesn't work (since obj does exist but contains null or undefined for at least one property on our way to destructure 'firstLineOfCopy'const { main: { content: { copy: [ firstLineOfCopy ] } } } = obj || defaultObj// TypeError: Cannot read property 'Symbol(Symbol.iterator)' of undefined

The best way we have found to deal with this gotcha is to set inline defaults for each level of the destructuring, as we did in pattern 1 earlier.

// this works !!! but it's starting to get difficult to readconst { main: { content: { copy: [ firstLineOfCopy ] = ['some text'] } = {} }  } = obj ||  defaultObj

Defaults only apply to undefined, not to null

As promised earlier, let’s take a quick look at how we deal with the case where one or more of the properties within the object are null .

Is there a way to destructure from an object that contains null values and not have it fail?

Going back to an earlier example, let’s set content to null:

const obj = {
main: {
content: null
}
}

Our utility function getPropValue can handle null property values without falling over and spilling coffee everywhere:

const title = getPropValue(obj, 'main.content.title')
// null

Object destructuring is a little trickier.

First, a bit of a refresher on what null is. A value or property cannot be null unless it has been explicitly set to null. That is why default values apply to undefined and not to null. A property that has not been set is undefined, but if it is null it has been explicitly set to null at some point, and javascript sees that as the intent of the developer and does not overwrite that value with a default value.

So this means that using our earlier technique of setting a default for content would fail:

// if the content property in main in obj is null, this will failconst { main: { content: { title } = { title: 'default title' } } } =  obj// TypeError: Cannot destructure property `title` of 'undefined' or 'null'.

To get around this, we can split it over multiple lines using || {} as a guard against a falsy object value such as undefined or null (be careful if some other such as false, 0, or '' is a valid return value)

// if the content property in main in obj is null, this will still workconst { main } =  obj || {}
const { content } = main || {}
const { title } = content || { title: 'default title' }
// 'default title'

This works best if we know which property we can expect to have a null value, since we’ll only need to ensure that we split that property out and write it on its own line.

Otherwise, in such cases, once again we found that in some cases we are better off using getPropValue

const title
= getPropValue(obj, 'main.content.title') || 'default title'

Conclusion

Phew! A quick not-so-quick summary of some of the things we’ve learned through our use of object destructuring over the last couple of years.

To sum up, when we do use object destructuring, we tend to use pattern 4 (repeated below for reference) whenever possible when we know the object we are destructing from has the correct shape. If we can’t be sure of that, then we sprinkle in some defaults as in pattern 1.


One thing to bear in mind is that, as with most any coding paradigm, destructuring is not the best pattern to use in all cases. For destructuring statements that are more complex and difficult to both write and read, we find that getPropValue can often achieve the same thing in an easier to read format.

Have a look at the example below. It could be that in this particular case we know for sure that obj contains a property main that is never null or undefined when we come to destructure it. But let’s say that we can’t be sure that main contains the content property so we assign a default to it inline:

const defaultObj = {
main: {
content: {
title: 'defaultTitle',
description: 'defaultDescription'
}
}
}
const { main: { content: { title } = { title: 'default title' } } }
= obj || defaultObj

This is already beginning to get difficult to read. When we have to destructure a more deeply nested property and set defaults as in the previous example, it can quickly get out of hand. If object destructuring is to be used as syntactic sugar to make the code easier to read, for ourselves now and for future maintainers of the code who may not be as familiar with destructuring, we always have to question whether we are achieving that goal or if there is a better way.

We’ve found that object destructuring isn’t a ‘golden hammer’. When the meaning starts to get lost in a sea of defaults and curly brackets, it’s time to take a step back and consider a different approach, which may mean falling back to using our tried and trusted getPropValue utility function:

const title
= getPropValue(obj, 'main.content.title') || 'default title'

or even simple dot notation:

const title = obj.main.content.title || 'default title'

This blog post was in part inspired by the ‘learning in public’ philosophy of Kent C.Dodds. Even though we feel we’ve got a lot of things right (otherwise we wouldn’t be doing them in the way we are), we are always open to and welcome comments on techniques we could improve upon, features we might have missed, or simply a better way of doing things. So please, don’t feel shy. Drop a comment below and let’s all learn together.


Bernard Leech is a Javascript Developer at Crunch. Bernard has a background in electronic engineering, before becoming a Javascript Developer. When not developing, Bernard enjoys writing EDM music using Propellerhead products.

Find out more about the Technology team at Crunch and our current opportunities here.

Crunch Tech

Written by

We are the Crunch Technology Team. https://www.crunch.co.uk/

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade