Learning ES6: Destructuring

After learning all about default parameters and rest & spread operators, let’s continue the syntactic sugar train in our Learning ES6 series with destructuring in ECMAScript 6.

TL;DR

Destructuring makes it easier to work with objects and arrays in JavaScript. Using a pattern syntax similar to object and array literals, we can poke into data structures and pick out the information we want into variables.

// object pattern matching
let {lName, fName} = {fName: 'John', age: 15, lName: 'Doe'};

// array pattern matching
let [first, second, third] = [8, 4, 100, -5, 20];

// output: Doe, John
console.log(lName + ', '+ fName);

// output: 100, 4, 8
console.log(third, second, first);

As you can see, we can store properties of an object or elements of an array using pattern matching. You can find many more examples in the destructuring code examples which are part of the Learning ES6 Github repo.

If you’re finding destructuring intriguing (or maybe confusing), keep on reading for a more in-depth explanation.

Some background

JavaScript developers spend quite a bit of time extracting data from arrays and objects. Destructuring allows variable binding via pattern matching with support for both arrays and objects. The patterns closely resemble those of array and object literals. Destructuring is also fail-soft, meaning that values not found return undefined instead of an error.

But first, let’s take a look at how we deal with objects and arrays currently. There is a verbose way of defining objects and arrays:

var config = new Object(),
	myArray = new Array();

config.delay = 500;
config.title = 'Hi!';
config.info = new Object();

// add to nested object
config.info.name = 'Elijah';

myArray.push(1);
myArray.push(new Array());
myArray.push(true);

// add to nested array
myArray[1].push('hello');

This can be significantly shortened into one-liners using literal syntax:

var config = {delay: 500, title: 'Hi!', info: {name: 'Elijah'}},
	myArray = [1, ['hello'], true];

Similarly, there’s a verbose way of retrieving data from objects and arrays:

var config = {delay: 500, title: 'Hi!', info: {name: 'Elijah'}},
	KEY = 'info',
	delay = config.delay,
	configTitle = config['title'],
	info = config[KEY],
	fullName = config.name,

	myArray = [1, ['hello'], true],
	first = myArray[0],
	second = myArray[1],
	secondNest = second[0],
	third = myArray[2];

ES6 provides a new syntax for quickly retrieving property values from objects and elements from arrays. This process is called destructuring assignment and we’ll spend the rest of our time looking at examples.

Object destructuring

Object destructuring assignment uses an object literal pattern on the left hand side of an assignment operation. Let’s convert the object portion of the ES5 example above using ES6 destructuring (and let):

let config = {delay: 500, title: 'Hi!', info: {name: 'Elijah'}},
	{delay, info, title} = config;

// output: {name: 'Elijah'}, 500, 'Hi!'
console.log(info, delay, title);

We were able to store references to the 3 property values within config into variables with names that matched the property keys of config. This is actually the shorthand syntax for object destructuring. If instead we wanted to use different variable names, we can use the full syntax:

let config = {
		delay: 500,
		title: 'Hi!',
		info: {name: 'Elijah'}
	},
	{
		info: one,
		title: two,
		empty: three,
		delay: four
	} = config;

// output: {name: 'Elijah'}, 'Hi!', undefined, 500
// missing properties have `undefined` value
console.log(one, two, three, four);

We were able to use an alias for the info property from config, naming it one. We did the same for title (two) and delay (four). Notice that we also tried to get the empty property out of config but since it doesn’t exist three is undefined. This works exactly how it would in ES5 with dot- or bracket-notation.

Nested object destructuring

Destructuring also supports nesting. So if we wanted to get the value of config.info.name (fullName variable in the early example) we can do the following:

let config = {delay: 500, title: 'Hi!', info: {name: 'Elijah'}},

	// `delay` is using shorthand syntax mixed in w/
	// full syntax
	{
		info: {name: fullName},
		delay,
		title: configTitle
	} = config;

// output: 'Elijah', 'Hi!', 500
console.log(fullName, configTitle, delay);

The destructure pattern we specify in order to assign to fullName is a nested object. If we were ok with the values of config.info.name going into a variable named name, we could’ve simply done: {info: {name}}. It’s worth pointing out that you can not assign both parent and child properties at the same time. So in the example, you can either get the info property or its child properties. Not both.

We learned earlier that destructuring is fail-soft so missing properties result in undefined values. This doesn’t apply, however, when trying to assign a missing child property whose parent property is also missing:

let options = {},

	// `delay` would be `undefined`, but trying
	// to assign to `name` is an error
	// since `options.info` is already `undefined`
	{delay, info: {name}} = options;

Computed values in object destructuring

Destructure patterns also support using computed values in the full syntax:

const KEY = 'empty';
let options = {delay:500, empty:true, title:'Hi!'},
	{[KEY]: empty, delay, title} = options;

// outputs: 'Hi!', 500, true
console.log(title, delay, empty);

Object destructuring gotchas

You don’t have to exclusively use destructuring when declaring a variable via let or const (or var if you’re still in to that sort of thing). You can use it for normal assignments to variables that have already been declared. However, when you do assignment-only object destructuring, you have to wrap the entire statement in parentheses:

let a,
	b = {};

// some code

( {a, b: b.count} = {a: 1, b: 2} );

// output: 1, {count: 2}
console.log(a, b);

If you leave off the parenthesis you will get a SyntaxError. This is because without the parentheses the statement looks like an invalid code block to the JavaScript engine. You should also notice that in the assignment, we assigned directly to b.count. Basically, anything that can accept an assignment can be used in a destructure pattern.

Array destructuring

Array destructuring works much the same way as object destructuring except you use an array literal destructure pattern for the left hand side assignment operation. As we saw earlier, the ES5 way of storing elements of an array into a variable can be pretty cumbersome. We can rewrite it in ES6 as such:

let myArray = [1, ['hello'], true],
	[first, second, third] = myArray;

// output: 1, ['hello'], true
console.log(first, second, third);

There’s only one syntax with array destructuring and that’s the shorthand syntax because the index that the variable will map to is implied by its order in the array literal destructure pattern.

Nested array destructuring

If instead we wanted to get the element within the second array, we can use a nesting pattern similar to what we did with object destructuring:

let myArray = [1, ['hello'], true],
	[first, [secondNest], third] = myArray;

// output: 1, 'hello', true
console.log(first, secondNest, third);

Skipping indices in array destructuring

Even though the array literal destructure pattern is order-dependent, we can still pick and choose which elements of the array we want stored as variables by skipping indices:

let sequence = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34],
	[first, , third, fourth, , , seventh] = sequence
;

// output: 0, 1, 2, 8
console.log(first, third, fourth, seventh);

Pretty cool, huh?

Mixed object & array destructuring

As you might have already guessed, we can mix object and array literal destructure patterns. This can come in very handy when we’re pulling out various pieces of data from a JSON object because we no longer have to navigate the entire structure.

let json = {
		shapes: ['circle', 'square', 'triangle'],
		colors: 5,
		fill: true,
		author: {
			firstName: 'Ben',
			lastName: 'Ilegbodu',
			city: 'Pittsburg'
		}
	},
	{
		fill,
		author: {lastName, firstName, city},
		shapes: [, secondShape],
		colors: numColors
	} = json;

// output: true, square, 5
console.log(fill, secondShape, numColors);
// output: Ilegbodu, Ben, Pittsburg
console.log(lastName, firstName, city);

Now that’s powerful stuff.

Destructuring use cases

The mixed object and array destructuring with JSON data is a real-world use case. Here are a few more.

Swapping values

What happens if we need to swap values in two variables? In the standard answer, we end up needing a third temp variable:

var a = 1,
	b = 2,
	temp;

temp = a;
a = b;
b = temp;

However, now that we have destructuring, we can use array destructuring to quickly swap two numbers without the need for a temp variable:

let a = 1,
	b = 2;

[b, a] = [a, b];

See what we did there? We constructed an array using the array literal syntax with two elements: a and b. Then using array destructuring we assigned the first element of the newly created array into b and the second element into a. The result is that the variables’ values have swapped. Now technically we did still create a temporary array, but we never had to store it into a variable.

Destructuring class objects

Class objects are similar to object literals except they are created using prototype. As long as properties of the class object are accessible via dot- or bracket-notation, they will also be available via object destructuring. Here’s an example using the Location object:

let {
		protocol: scheme,
		host: domain,
		pathname: path,
		search: query,
		hash,
		href: url
	} = location;

// output: true
console.log(
	(scheme + '//' + domain + path + query + hash) == url
);

Destructuring return values

Many methods on JavaScript APIs return arrays (such as exec on RegExp objects or split on String objects). You can use array destructuring to get the array values into variables quickly:

let [, areaCode, exchange, lineNumber] =
	/^(\d\d\d)-(\d\d\d)-(\d\d\d\d)$/
		.exec('650-555-1234');

// output: 650, 555, 1234
console.log(areaCode, exchange, lineNumber);

As we know, exec returns an array of the matching groups with the first element being the entire match. We’re not interested in that first piece, so we skip it and get the remaining three parts. You can imagine what this code looks like in ES5.

Handling multiple return values

Sometimes a function needs to return multiple pieces of data. This typically packaged together in an object literal or as an array tuple. Once again we can use destructuring to easily get out the data we care about.

Let’s say we have a find method that takes as parameters an array list and a string token, where it finds the first string in list that contains token.

function find(list, token) {
	for (let i = 0; i < list.length; i++) {
		if (list[i].indexOf(token) > -1)
			return {index: i, val: list[i]};
	}

	// match not found
	return {index: -1, val: undefined};
}

Now a caller of find might care about the index at which token was found or it might care about the string itself. That’s why find returns an object literal containing both the index number and the val string.

If the caller needs a reference to both pieces of information, they can do:

let fruits = ['apple', 'grape', 'peach', 'pear'],
	{index, val} = find(fruits, 'ape');

Or if they just care about the val:

let fruits = ['apple', 'grape', 'peach', 'pear'],
	{val} = find(fruits, 'ape');

Or if they care just about the index:

let fruits = ['apple', 'grape', 'peach', 'pear'],
	{index} = find(fruits, 'ape');

Array destructuring & rest operator

We previously learned about the rest operator in the context of a function header. But it doesn’t only work with functions. It can also be used to simplify array manipulation.

For example we can turn this in ES5:

// ES5
var list = [9, 8, 7, 6, 5],
	first = list[0],
	second = list[1],
	rest = list.slice(2);

// output: [7, 6, 5], 8, 9
console.log(rest, second, first);

To this in ES6 using the rest operator with array destructuring:

// ES6
let list = [9, 8, 7, 6, 5],
	[first, second, ...rest] = list;

// output: [7, 6, 5], 8, 9
console.log(rest, second, first);

We can replace the slice call as well as the individual array indexing into a simple destructure pattern.

Destructured parameters

We can actually using destructuring in function headers too. Let’s pretend we needed to implement an ajax method that takes the URL endpoint as well as a bucket of configuration options. In ES5, this would look something like:

// ES5
function ajax(url, options) {
	var method = options.method,
		delay = options.delay,
		callback = options.callback;

	console.log(url, method, delay);
	setTimeout(
		function() { callback('DONE!'); },
		delay
	);
}

ajax(
	'https://api.eventbrite.com/get',
	{
		delay: 2000,
		method: 'POST',
		callback: function(message) {
			console.log(message);
		}
	}
);

As you can see, we are trying to get values out of the options object to use in our ajax method. Just looking at the method, it’s not immediately clear what properties can be in options. Documentation would be needed. The reason the options object is used instead of individual parameters is to not explode the arity of the function and because some of the properties of options could be optional. It’s also how named parameters are accomplished in ES5.

In ES6, there is still no official approach to named parameters, but we can get a bit closer using object destructuring of function parameters:

// ES6
function ajax(url, {method, delay, callback}) {
	// `method`, `delay` & `callback` are
	// destructured variables

	console.log(url, method, delay);
	setTimeout(
		() => callback('DONE!'),
		delay
	);
}

ajax(
	'https://api.eventbrite.com/get',
	{
		delay: 2000,
		method: 'POST',
		callback: function(message) {
			console.log(message);
		}
	}
);

No more need to declare and assign the individual method, delay and callback variables.

What if we want to support options being optional? Well in ES5 the implementation would have to start with:

// ES5
function ajax(url, options) {
	// default `options` to empty object
	// so var declarations don't throw error
	// if `options` is `undefined`
	options = options || {};

	// var declarations and
	// rest of the function...
}

Well guess what? We can use default parameters to simply default options to the empty object in ES6:

// ES6
function ajax(url, {method, delay, callback}={}) {
	// default {} is used to allow
	// object to be unspecified w/o
	// causing an error

	// rest of the function
}

See what we did there? We combined default values with parameter destructuring. If options is left unspecified or undefined, it’ll trigger the default value of {} which then gets destructured into method, delay and callback. If we didn’t provide a default value and didn’t specify options in the call to ajax, we would get an error because we’re trying to destructure undefined. It’s good practice to specify a default value for object destructured parameters.

Ok, what if we wanted to have default values for method and delay? They are within options. In ES5, we’d do something like:

// ES5
function ajax(url, options) {
	options = options || {};

	// default the values of `method` and
	// `delay`
	var method = options.method || 'GET',
		delay = options.delay || 1000,
		callback = options.callback;

	// rest of the function...
}

In ES6, we can use default values within our object destructure pattern to accomplish the property defaulting:

// ES6
function ajax(url, {method='GET', delay=1000, callback} = {}) {
	// default values w/in destructure pattern

	// rest of the function
}

Nested default values! Cool, huh? That’s the power of ES6.

JavaScript engine support

According to the ECMAScript 6 Compatibility table, the following JavaScript engines support destructuring:

  • Babel
  • Traceur
  • TypeScript
  • Firefox (does support default values in destructure patterns yet)
  • Safari (partial support)

It’s surprising to see that Chrome doesn’t yet support destructuring. Neither does Node.js. This is also the first feature we’ve covered that Microsoft’s Edge doesn’t support either. My guess is that this feature may be low priority for these vendors since it’s basically 100% syntactic sugar. But it’s such a great feature!

Additional resources

You can check out the Learning ES6 examples page for the Learning ES6 Github repo where you will find all of the code used in this article running natively in the browser (for those that support destructuring). There are also examples running through Babel and Traceur transpilation.

You can also practice everything you’ve learned on ES6 Katas. It uses a TDD (test-driven development) approach for you to implement ES6 features such that all of the tests pass. I highly recommend it!

Other super helpful resources:

Coming up next…

So far in the Learning ES6 series we’ve looked at only syntactic sugar features in ECMAScript 6. But ES6 isn’t solely syntactic sugar. There’s actually new functionality as well. Next time we’ll take a look at the new for-of operator. Until then…

FYI

This Learning ES6 series is actually a cross-posting of a series with the same name on my personal blog, benmvp.com. The content is pretty much the exact same except that this series will have additional information on how we are specifically leveraging ES6 here in Eventbrite Engineering when applicable. I’ll also be tackling the features in a different order than I did in my personal blog. The original destructuring blog post can be found here.

Leave a Reply

Your email address will not be published. Required fields are marked *