April 4, 2016

Destructuring, Practically

What is destructuring?

Destructuring is a new feature that was added to the spec for ES2015. It makes it possible to extract properties and elements from arrays and objects.

let obj = { someProp: 1, other: 'string' }
let { someProp, other: secondProp } = obj

console.log(secondProp) // writes out 'string'

Notice how we can extract things from an object into local variables, renaming if desired. In this case, we renamed other to secondProp while destructuring.

We can also destructure arrays:

let [first, second] = [1, 2, 3, 4]

console.log(second) // writes out '2'

There are a ton of more complicated things you can do as well, including default values, nested destructuring, rest params, and more. We’ll touch on a few of these, but you can also find more detail here: http://www.2ality.com/2015/01/es6-destructuring.html

Why is destructuring useful?

You may have already heard of destructuring before. But that doesn’t mean you use it when you actually write code. Sometimes we don’t incorporate new stuff because of habit, but sometimes it’s just because we don’t know why or where to use it. Here’s a few ideas on practical usages of destructuring:

Simplify parameter lists

It can be a pain passing more than 2 or 3 parameters into a function. An easy solution to this is to pass in an object instead. But then we have to constantly reference the parameters off of this object… unless we use destructuring.

function doSomeCoolStuff(options) {
   const { shouldItBeDone, howToDoIt, aFewCaveats } = options

   if (shouldItBeDone) {
      howToDoIt(aFewCaveats)
      // ...
   }
}

// Calling our function:
doSomeCoolStuff({
   shouldItBeDone: true,
   howToDoIt: myHowToDoItImplementation,
   aFewCaveats: listOfConcerns,
})

This makes it very simple to pass in multiple parameters and we can consume the parameters very simply inside our function. We can also specify parameter defaults if we want, using the property defaults idea listed next.

Property defaults

Sometimes we may not be certain that the properties we need are set on an object, but we don’t want to modify the original object. In this case, we can use defaults during destructuring to set default values without touching the object.

let myObject = { value = 15 }
const { message = 'myMessage', value = 5 } = myObject

// Prints `myMessage = 15`
console.log(message + ' = ' + value)

This is a really nice way to provide defaults without risking side effects by modifying an object that you may want to treat as immutable.

Human readable array items

When working with arrays, we may want to use specific elements. Destructuring can let us descriptively name these elements so that our logic is declarative.

Instead of:

const cols = row.split(',')

handleCol(cols[1], cols[2], cols[3])
process(cols[4])

We can use destructuring:

const [/* id field not used */, name, description, location, request] = row.split(',')

handleCol(name, description, location)
process(request)

Much more descriptive.

Simplify deeply nested references

We can use destructuring to pull values out of nested object trees, letting us reference these values directly without the overhead and complexity of walking through the object chain every time.

let obj = { 
   nested: {
      val: 2,
      deeplyNested: {
         value: 1,
         name: 'myName',
      },
   },
}

let { nested: { val: outerValue, deeplyNested: { value: innerValue, name } } } = obj

// Prints `myName, your values are 1 and 2`
console.log(name + ', your values are: ' + innerValue + ' and ' + outerValue)

Swap values

Another popular usage for destructuring is a simple swapping of values. This demonstrates that arrays and objects can be destructured into existing variables as well as newly declared variables.

let a = 5, b = 10
[a, b] = [b, a]

// Prints `a = 10; b = 5`
console.log('a = ' + a + '; b = ' + b)

Array pattern matching

Functional programming languages often use pattern matching to recursively process the head item in an array. We can recreate this behavior using destructuring if desired. We can easily build a clone of array.prototype.reduce using this, although we will certainly want to consider if using vanilla reduce is more appropriate depending on our situation.

function process(array, processItem) {
   const [head, ...rest] = array

   // False values signal end
   if (head) {
      processItem(head)
      process(rest)
   }
}