11 Useful Ways to Make Your ES6+ JavaScript More Concise

Tricks with the Spread Operator and Destructuring Assignment Syntax

Published on
Apr 15, 2020

Read time
10 min read

Introduction

Among the many useful features introduced in ES6, few have the power to knock off as many lines of traditional JavaScript code as the spread operator and destructuring assignment syntax. In ES5, for example, you may have required a dedicated function to filter out unique values. Now, it’s possible in a single line of code.

In this article, we’ll look at several occasions when the spread operator and destructuring can make your code more concise. But first, let’s recap what these features do with some basic examples.

What is destructuring assignment syntax?

This is a way concise way to turn values from arrays or properties from objects into variables. We’ll start with a couple of examples of array destructuring.

const [a, b, c] = [1, 2, 3];

console.log(a); // 1
console.log(b); // 2
console.log(c); // 3

[firstName, lastName] = "Joe Bloggs".split(" ");

console.log(firstName); // "Joe"
console.log(lastName); // "Bloggs"

In fact, destructuring assignment syntax can be used with any iterables (such as strings or sets), as long as they’re on the right-hand side of the expression.

const [a, b, c] = "ABC";

console.log(a); // "A"
console.log(b); // "B"
console.log(c); // "C"

const [d, e, f] = new Set([1, 2, 3]);

console.log(d); // "1"
console.log(e); // "2"
console.log(f); // "3"

Used with objects, the basic syntax looks like this:

const state = { a: 1, b: 2, c: 3 };

const { a, b, c } = state;

console.log(a); // 1
console.log(b); // 2
console.log(c); // 3

Note that, if using let to declare variables, you may bump into an error if you later try to assign a value to them using destructuring assignment syntax.

To fix this, simply surround the statement in parentheses:

let a, b, c;
const state = { a: 1, b: 2, c: 3 };

{ a, b, c } = state;    // This will lead to a SyntaxError
({ a, b, c } = state);  // This works!

Destructuring assignmentThe destructuring assignment syntax is a JavaScript expression that makes it possible to unpack values from arrays, or…developer.mozilla.org

What is the spread operator?

This is a way concise way to expand values from arrays or properties from objects into variables. To use it, put three dots ... before an array (or any other iterable) or an object.

The simplest use case for spread syntax is passing multiple arguments to functions. Take the function sum below.

const sum3 = (x, y, z) => {
  return x + y + z;
};

Instead of triggering the function with individual argument s— as in sum(1, 2, 3) — we could instead use spread syntax to concisely pass in an entire array.

const arr = [1, 2, 3];

sum3(...arr); // 6
Math.max(...arr); // 3

Alternatively, we can also use spread syntax when we define the sum function, allowing us to easily accept a dynamic number of arguments.

const sum = (...nums) => {
  return nums.reduce((a, b) => a + b);
};

Now, instead of using spread syntax when we call the function, we can pass in a dynamic number of arguments.

sum(1, 2, 3); // 6
sum(1, 2, 3, 4, 5); // 15
sum(1, 2, 3, 4, 5, 6, 7); // 28

Now we understand the basics, let’s dig into some of the other ways we can take advantage of the spread operator and destructuring assignment syntax.

Spread syntaxSpread syntax allows an iterable such as an array expression or string to be expanded in places where zero or more…developer.mozilla.org

1. Combining arrays

In the early days of JavaScript, we’d have to iterate over each array to push each item into a new one. The apply method could be used to provide a concise solution, but it only worked on two arrays at a time.

var arr1 = [1, 2, 3];
var arr2 = [4, 5, 6];
var result = [];
result.push.apply(arr1, arr2);

Since ES3, we’ve had the concat method for combing arrays, which is a lot more convenient.

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const result = arr1.concat(arr2); // [1, 2, 3, 4, 5, 6]

But the spread operator provides another highly concise — and arguably, more readable — option:

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const result = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]

2. Filtering unique values from an array

Before the introduction of Sets, filtering unique values required the creation of a custom function:

function distinct(value, index, self) {
  return self.indexOf(value) === index;
}

var arr = [1, 1, 1, 2, 2, 3, 3, 3, 4, 5, 5];
var uniqueArr = arr.filter(distinct); // [1, 2, 3, 4, 5]

Since Sets may only contain unique values, this does the job of filtering unique values in a more concise (and more performant way). What the spread operator provides is a way to quickly convert back from a Set to an array:

const arr = [1, 1, 1, 2, 2, 3, 3, 3, 4, 5, 5];
const uniqueArr = [...new Set(arr)]; // [1, 2, 3, 4, 5]

3. Combining objects

Merging multiple objects used to be an even more cumbersome challenge. Prior to ES6, developers were forced to rely on helper functions, like the one below.

function merge(obj1, obj2) {
  for (var key in obj1) {
    obj1[key] = obj2[key];
  }

  return obj1;
}

More recently, it’s been possible to use Object.assign, like so:

Object.assign({}, obj1, obj2);

But the spread operator makes this even simpler:

const obj1 = { a: 1 };
const obj2 = { b: 2 };
const result = { ...obj1, ...obj2 }; // { a: 1, b: 2 }

Note that, if objects have the same property, its value will be that of the final object declared with that key.

const obj1 = { a: 1 };
const obj2 = { b: 2 };
const obj3 = { b: 3 };
const result = { ...obj1, ...obj2, ...obj3 }; // { a: 1, b: 3}

4. Cloning arrays and objects

JavaScript developers have been able to easily clone arrays for some time, using splice, so the spread operator brings only a small improvement:

const arr = [1, 2, 3, 4, 5];
const arrClone1 = arr.slice();
const arrClone2 = [...arr];

But for objects, the saving is much greater. We now have Object.assign and the spread operator to create shallow copies of objects:

const obj = { a: 1, b: 2, c: 3 };
const objClone1 = Object.assign({}, obj);
const objClone2 = { ...obj };

However, in the past, it was extremely cumbersome to clone objects. To give you an idea of how much, here’s MDN’s suggested polyfill for Object.assign:

5. Removing an object property

Instead of relying on the delete keyword, the spread operator and destructuring assignment together allow us to remove fields from objects non-destructively.

Let’s imagine we had a user object in our database with name, email and _id fields.

const user = {
  name: "Joe Bloggs",
  email: "joebloggs@example.com",
  _id: "3ef7c",
};

We want to clone our user, but we don’t want any duplicates in the _id field, so we need to remove that field. Before, we’d need to use Object.assign and the delete keyword.

const newUser = Object.assign({}, user);
delete newUser._id;

But with destructuring assignment, we can instead define newUser like this:

const { _id, ...newUser } = user;

Similarly, to remove properties with dynamic variables, we can use the removed keyword:

const propertyToDelete = "_id";
const { [propertyToDelete]: removed, ...newUser } = user;

6. Looping through an object

Originally, if we wanted to loop through the key and value pairs of an object, we’d need to rely on something like this:

for (let i = 0; i < Object.entries(obj).length; i += 1) {
  const key = obj[i][0];
  const value = obj[i][1];
  console.log(`${key}: ${value}`);
}

With a for ... of loop and destructuring assignment, there’s a simpler alternative, saving us from dealing with nested arrays:

for (const [key, value] of Object.entries(obj)) {
  console.log(`${key}: ${value}`);
}

7. Using defaults to fill in missing values

It’s possible to specify default values inside a destructured array or object. For example, in the object below, if the createdAt property doesn’t exist on a given object, we should set it to the current date:

const user = { name: "Jean", surname: "Doe" };
const { name, surname, createdAt = new Date() } = user;

console.log({ name, surname, createdAt });

We can also provide defaults if our data is stored in an array:

const user = ["Jean", "Doe"];
const [name, surname, createdAt = new Date()] = user;

console.log({ name, surname, createdAt });

In addition, we could use our defaults to trigger a function every time we encounter a missing value. Below, for example, we can use the prompt function to fill in a missing surname:

const [name = prompt("name?"), surname = prompt("surname?")] = ["Jean"];

8. Accessing nested properties

Take the following object:

const player = {
  name: "Roger Federer",
  sport: "Tennis",
  grandSlamVictories: {
    australianOpen: 6,
    frenchOpen: 1,
    wimbledon: 8,
    usOpen: 5,
  },
};

If we want to access any properties of grandSlamVictories, we can do so in a single line with destructuring:

const {
  grandSlamVictories: { australianOpen },
} = player;

Multiple levels of nesting

We can also access properties nested multiple levels deep. For example, imagine we want to access name and the stats for Wimbledon from the object below:

const player = {
  name: "Roger Federer",
  sport: "Tennis",
  grandSlamVictories: {
    australianOpen: { won: 6, played: 21 },
    frenchOpen: { won: 1, played: 18 },
    wimbledon: { won: 8, played: 21 },
    usOpen: { won: 5, played: 19 },
  },
};

We can use:

const {
  grandSlamVictories: {
    wimbledon: { won, played },
  },
} = player;

console.log(`${won} won out of ${played} played`);

Or, what if we wanted to access australianOpen, frenchOpen, wimbledon and usOpen as separate variables? We could, of course, do something like this:

const {
  grandSlamVictories: australianOpen,
  frenchOpen,
  wimbledon,
  usOpen,
} = player;

But, by adding the spread operator, we can save ourselves several characters:

const {
  grandSlamVictories: { ...grandSlamVictories },
} = player;

Nested arrays and objects

Finally, we can also mix arrays and objects when destructuring nested properties. Take, for example:

const loc = {
  name: "21 Jump Street",
  coordinates: [14.608923, 121.0877606],
};

To access the latitude and longitude as variables, we can use:

const {
  coordinates: [lat, long],
} = loc;

9. Swapping variables

Let’s imagine we’re working with coordinates on a mobile phone screen. We need to know it’s x coordinate and y coordinate. But what if we wanted to change the orientation of our phone and swap them around?

Traditionally, we’d be forced to introduce a third variable to assist with this task.

let x = 1080;
let y = 1920;

if (landscape) {
  let prevX = x;
  x = y;
  y = prevX;
}

But destructuring assignment can reduce three lines to just one, and remove the need for an extra variable:

let x = 1080;
let y = 1920;

if (landscape) {
  [x, y] = [y, x];
}

10. Converting a NodeList to an array

This is a small saving, but one that I use a lot when writing front-end code. When you use document.querySelectorAll, you get a NodeList rather than an array, which is often much easier to work with:

[...document.querySelectorAll("div")];

11. Passing options to functions

We’ve already spoken about how the spread operator allows us to easily send multiple arguments to a given function. But making the most out of destructuring syntax can benefit a lot of functions.

When designing functions from scratch, we’re faced with a choice between using multiple distinct arguments are passing our arguments to the function via an object.

Usually, the former has had the advantage of simplicity. But it also has some limitations:

const numberToCurrency = (number, currency = "USD", dp = 2) => {
  const symbols = {
    USD: "$",
    GBP: "£",
    EUR: "€",
    INR: "₹",
    JPY: "¥",
  };

  return symbols[currency] + parseFloat(number).toFixed(dp);
};

Imagine we want to leave the default currency as "USD" but change the decimal places to 0. We can’t write numberToCurrency(10.35, , 0) so we’re forced to use undefined to “skip” the currency argument:

numberToCurrency(10.35, undefined, 0);

This is a simple example, so it’s not much hassle. But imagine a more complex function with 10 arguments, 9 of which could be undefined.

We can get around this by using an options object as the second argument and destructuring helps keep this code concise:

const numberToCurrency = (number, { currency = "USD", dp = 2 }) => {
  const symbols = {
    USD: "$",
    GBP: "£",
    EUR: "€",
    INR: "₹",
    JPY: "¥",
  };

  return symbols[currency] + parseFloat(number).toFixed(dp);
};

It’s a small change (we only introduced curly braces), but we no longer have to account for undefined arguments:

numberToCurrency(10.35, { dp: 0 });

React

Finally, a quick extra for those using React. The same principles discussed above apply to React Components, which are ultimately functions (including class components).

We can use some of this article’s techniques, for example, to pass all the props from a parent component to one of its child components:

import React from "react";

function ParentComponenet(props) {
  return <ChildComponent {...props} />;
}

export default ParentComponenet;

Or, image the parent component's props object includes a url which the child component doesn’t need. We can use destructuring to take url out of props, before then passing everything else to the child component:

import React from "react";

function ParentComponenet({ url, ...props }) {
  return <ChildComponent {...props} />;
}

export default ParentComponenet;

Overall, I hope you found this a useful summary of some of the ways that destructuring assignment syntax and the spread operator can improve your JavaScript — making it more concise, more readable and sometimes more performant too!

© 2024 Bret Cameron