JS destructuring in ES2015+
By Christophe Porteneuve • Published on 31 January 2022 • 7 min

Cette page est également disponible en français.

Welcome to the fourth installment of our Idiomatic JS series.

ES2015 (long known as ES6) brought us a ton of new stuff, including what I like to call the “Holy Trinity:” destructuring, rest / spread and default values. We’ll cover each one in its own dedicated article.

With destructuring, there’s this fascinating thing where it seems to take more time to truly “percolate” through our little gray cells, in order to truly be used to its full potential in our code. It often takes months, sometimes even years, by sheer virtue of going through our code again and again, before we truly use it everywhere it’s beneficial. The syntax can also sometimes be confusing. In this article, we’ll do a deep dive into this subject, and try to provide you with enough pro tips to gain the most from it.

ES2015 (longtemps appelé « ES6 ») nous a apporté énormément de choses, dont ce que j’aime appeller la « Sainte Trinité » : la déstructuration, le rest/spread, et les valeurs par défaut. Nous allons les voir chacun dans son propre article.

Why should I destructure?

The main goal of destructuring is to grab multiple members at once from a data structure (as in, “in a single statement”). That data structure is necessarily an object: a primitive only holds one value. This helps us avoid multiple declarations or assigments, and spares us from having to index arrays with many “magic numbers” or looking up many properties in sequence from the same source object.

We’ll look at the syntax for these use cases in detail, but honestly, which do you prefer? That kind of code:

function process(params) {
// I love using "params" all over the place. That gives me life.
seedNetwork({
credentials: params.credentials,
// I love magic numbers too…
frontLayer: params.seedData[0],
midLayer: params.seedData[1],
backLayer: params.seedData[2],
})
params.notifier.notify('seeded')
}

…or that kind of code:

function process({ credentials, notifier, seedData }) {
const [frontLayer, midLayer, backLayer] = seedData
seedNetwork({ credentials, frontLayer, midLayer, backLayer })
notifier.notify('seeded')
}

I know what I like best.

(If you wonder where all the colons went, you probably haven’t read our article on shorthand properties.)

Where can I destructure?

Destructuring can be used wherever there is an assignment, be it explicit or implicit.

Explicit assigments

Explicit assignments are easy to spot: there’s an assignment operator (=), which can happen within a declaration or expression.

Most folks think you have to be in a declaration (e.g. with const, as const is the new var). Something like this:

// How about a hook, pal?
const [name, setName] = useState('')

// Like Redux?
const { email, password } = action.payload

And indeed, this is the majority case. However, you can absolutely destructure towards existing identifier, as long as they can be rassigned (hence not const-declared). Like in the following writer accessor, for instance:

class Person {
constructor(first, last) {
this.first = first
this.last = last
}

set fullName(value) {
;[this.first, this.last] = value.split(' ')
}
}

In the code above, we destructure directly towards property of the current object (this), which don’t need to be declared ahead of time.

You might be confused by that semicolon just before the destructuring? This is because, in the semicolon-free style (that we’ve favored for 10+ years), thereis an edge case that warrants a semicolon ahead of an opening square bracket: JS might believe this is a dynamic indexing (square bracket operator, as in items[2]) on the expression ending the previous code line.

As we’re at the beginning of a block here, there is no ambiguity and it would work just fine without the semicolon; but we automatically format our code with Prettier, and it tries to help us avoid issues should we later insert a line of code between the opening of the block and our existing line. By forcing the semicolon from the get-go, we’re safe and it reduces later Git diffs should a line be inserted above.

Implicit assigments

Implicit assigments come from the semantics of the language and don’t need the = operator. This boils down to two scenarios:

  • Parameter names in a function signature (they are implicitly assigned arguments at call time), and…
  • Contents of a destructuring (every element is implicitly assigned a member of the source object).

Circling back to syntax for a bit, here are examples of each case:

const DEFAULT_SETTING = Object.freeze({ name: null, type: null, value: '' })

// In a signature
function Settings({ settings }) {
// Nested within another destructuring — fear not, we'll cover that later 😉
const [{ name, type, value }, setEditedSetting] = useState(DEFAULT_SETTING)
// …
}

Two types of destructuring

There are two types of destructuring:

  • name-based: we’re interested in the source object’s property by name, so order doesn’t matter. The source can be any object at all.
  • position-based: the source object must then be an iterable

Il existe deux types de déstructuration :

  • nominative : on s’intéresse aux propriétés de l’objet source par leurs noms, et l’ordre n’a donc aucune importance. La source peut alors être n’importe quel objet.
  • positionnelle : l’objet source doit alors être un itérable (the most famous ones being arrays), and we’re interested in properties by their “position,” or more accurately by their order in the iterability sequence of the source object.

In both cases, we destructure an object, which means you can’t destructure null or undefined, which are not (even if, for lousy legacy reasons, typeof null === 'object' 😩).

Name-based destructuring

Name-based destructuring is wrapped by the same delimiters that denote an object literal: curly braces (or curlies for short, { … }). Here’s a destructuring declaration:

// BEFORE
const first = person.first
const last = person.last

// AFTER
const { first, last } = person

Another example in a function signature:

function logNode({ nodeType, nodeValue }) {

}

As you could deduce from the underlying semantics (as in the “BEFORE” segment in the first example below), when the source property doesn’t exist, the created identifier will be undefined.

We sometimes need to use “local” name that is different from the source name, either because it would become too generic or abstract without its container object’s name to qualify it (e.g. size, name), or because we need to destructure multiple objects of the same shape (e.g. for property-based comparison functions).

We can then use destructuring aliases using a colon, which you really should read “as”, because the local identifier will be on the right-hand side of the colon (unlike an assignment or an object’s key-value pair, for instance, which makes this a bit confusing the first few times).

const people = [
{ name: 'Alice', age: 31 },
{ name: 'Bob', age: 24 },
]
// Ascending-age sort. Because we need to destructure `age` on both
// arguments in the comparator callback, we'll alias each one to avoid
// a naming conflict.
people.sort(({ age: a1 }, { age: a2 }) => a1 - a2)

// Or in a Node.js `require` where function names might become less
// obvious once they're not prefixed by their module name:
const { join: joinPaths } = require('path')

// Or in a Jest test that creates multiple snaphots with React Testing Library,
// by destructuring the `container` property each time:
it('should match snapshots', () => {
const { container: regular } = render(<Gauge value={50} />)
expect(regular).toMatchSnapshot('regular')

const { container: customMax } = render(<Gauge value={20} max={80} />)
expect(customMax).toMatchSnapshot('custom max prop')
})

Position-based destructuring

For position-based destructuring, the target is wrapped by the same delimiters are an array literal ([…]). Here are a few examples:

const people = ['Alice', 'Bob', 'Claire', 'David']
const [first, runnerUp] = people
// => first === 'Alice', runnerUp === 'Bob'

Array.from(people.entries()).map(([index, name]) => `${index + 1}. ${name}`)
// => ['1. Alice', '2. Bob', '3. Claire', '4. David']

for (const [index, name] of people.entries()) {
// …
}

This is especially useful because it provides readable names to the “positions” that are relevant to us, avoiding hard-to-maintain “magic indices” in the rest of the code, such as people[3].

(What was that? You don’t know forof?!)

Technically, you can skip elements in the iteration by not providing an identifier and just typing a comma, but that can result in pretty weird-looking code:

// ⚠ DON'T DO THIS!
const [, , third, fourth] = people
// third === 'Claire', fourth === 'David'

The only real use-case is about skipping the first element, for instance to get number-based capturing groups in a regular expression match result:

// Sure, it would be a lot nicer with named capture groups, but humor me here.
const REGEX_US_PHONE = /^\((\d{3})\)-(\d{3})-(\d{4})$/

const [, area, prefix, line] = '(412)-555-1234'.match(REGEX_US_PHONE)
// area ==== '412', prefix === '555', line === '1234'

What if I only want one piece of data?

In name-based destructuring, you could just grab one property, because it remains less repetitive than without destructuring, especially when the property has a long name:

// BEFORE
const authenticationToken = config.authenticationToken
const filter = req.query.filter
const name = props.name
// AFTER
const { authenticationToken } = config
const { filter } = req.query
const { name } = props

It’s also useful to simply avoid qualifying the property over and over, as with props in a React component.

On the other hand, for position-based destructuring, single-item will always look worse without being much shorter, especially beyond the first item:

// BEFORE (😐)
const winner = runners[0]
const line = captures[2]

// AFTER (🤮)
const [winner] = runners
const [, , line] = captures

So really: avoid doing it.

Nested destructuring

Inside of a destructuring, we have implicit assignments. This means we can destructure again, and it is sometimes a pretty good idea that remains legible thanks to context:

const pairs = [
[
{ name: 'Alice', age: 24 },
{ name: 'Bob', age: 31 },
],
[
{ name: 'Claire', age: 29 },
{ name: 'David', age: 27 },
],
]
for (const [{ name: n1 }, { name: n2 }] of pairs) {
console.log('pair:', n1, '+', n2)
}

function HistoryLine({
day,
goal: { name, units },
stats: [progress, target],
}
) {
// …
}

// JSX bit that uses this component.
// day === new Date(2022, 0, 31)
// goal === { id: 'xxx', name: 'Learn React', target: 5, unit: 'doc page' }
// stats === [0, 5]
const child = <HistoryLine day={day} goal={goal} stats={stats} />

Sometimes, you really want two handlings of a property you’re destructuring:

  1. the entire property (the full object it references), perhaps to forward it to some other bit of code, and
  2. nested destructuring of some of its “sub-properties”, so you can reference them in a more concise way in your own code.

You don’t have to choose, you can have it both ways! For instance, in our React PWA training course, there’s a component that does just that with its props:

export default function GoalTrackerWidget({
goal,
goal: { name, units, target },
// …
}) {
// …
<Fab /* … */ onClick={() => onProgress?.(goal)} />
// …
<Typography component='small'>
{`${progress} ${units} sur ${target}`}
</Typography>
// …
}

I love it 🤩.

There’s always more!

Besides our wealth of other articles, you may wish to look at our live training courses! In particular, if you like super-deep dives into JavaScript itself, we heartily recommend our 360° ES course!