ES6 Cool Stuffs — Destructuring me, plz!

Unicorn in destructuring

Our topic today will be about another look-like-it’s-simple key feature of ES6 — Destructure Assignment. In this article, I will try to make it concise and interesting even though it’s a wide topic. It all begins with an unicorn😄.


A good code practice

Let’s start with a very simple use case for code practice.

We have a method — say buildAnimal(), which needs 3 parameters — accessory, animal, color.

function buildAnimal(accessory, animal, color){...}

Now we need to add another parameter, e.g.hairType. What should we do ?

The obvious solution: add to the end of parameter list.

However, since we are good developers 😆, we want to write clean code — which means

“Functions should have a small number of arguments. No argument is best, followed by one, two, and three. More than three is very questionable and should be avoided with prejudice.”
Robert C. Martin’s Clean Code

Hence instead, we will do a little refactor here to combine all parameters into one object — animalData which will have following properties: accessory, animal, color, hairType. And now the method declaration becomes:

function buildAnimal(animalData){...}

From now on, there is no re-factor required if there is a need for new parameter. All can be passed as properties in animalData. Great, isn’t it?

Here comes the 2nd question — if we want to extract data from animalData, maybe for easy code-writing (think about animalData.accessory.foreHead which may cause a accidental dictation error). How do we do it?

Well, before ES6, we have no choice than extracting one by one:

function buildAnimal(animalData){
let accessory = animalData.accessory,
animal = animalData.animal,
color = animalData.color,
hairType = animalData.hairType;
...
}

Can be a little bit tiring, right? And quite a few repetitive code also 😅

Fortunately, after ES6, we finally a new mechanism to help — let’s have a warm welcome to De-structuring.

De-structuring Assignment

It’s known that JavaScript allows to construct object data with multiple properties at the time using object literal {}

let obj = { 
accessory: 'horn',
animal: 'horse',
color: 'purple',
hairType:'curly'
}

Based on the same concept, De-structuring mechanism is built to allow extracting multiple properties from object, elements from array, data from map/set, etc into distinct variables via patterns, all at once.

Pattern

There are always 2 parties in any de-structuring pattern:

  • De-structuring source: right-hand side assignment of =. It’s the source of values to be de-structured.
  • De-structuring destination: left-hand side assignment of =. It’s the pattern for de-structuring.

And we have different patterns to apply for each source data type, mainly:

Object de-structuring pattern

In the above example of buildAnimal, instead of repeating code for each mapped variable, we can re-write them in one single line as:

function buildAnimal(animalData){
let {accessory, animal, color, hairType} = animalData;
...
}

Code explanation: we define one variable as accessory, one as animal, so on and take their values from object animalData respectively — accessory property for accessory, animal property for animal, … and have their variables scoped to the context. In the end, we will have:

console.log(accessory);//horn
console.log(animal); //horse
...

You got it. The curly bracket on the left-hand side {} of the assignment isn’t meant to be a code block, it is syntax for object de-structuring pattern — using to define what values to un-pack from the source.

This is the simplest use case of de-structuring pattern. What about a deeper destructuring — say a nested values?

Assume we have the following object:

let person = { 
name: 'Maya',
age: 30,
phone: '123',
address:{
zipcode: 1234,
street: 'rainbow',
number: 42
}
}

We want to extract only values of zipcode, street, number from person. With the normal way, we will have to start with person.address.<property> and it is super annoying and tiring! With destructuring, we can simply do as:

let {address: {zipcode, street, number}} = person;
console.log(zipcode, street, number); //1234 rainbow 42

Or

let {zipcode, street, number} = person.address;
console.log(zipcode, street, number); //1234 rainbow 42

Code explanation: In both case, we define the variables zipcode, street, number and pick their values from nested values respectively of property address in person.

Easy peasy? Indeed.

Furthermore, some may argue that we will need to match the variable name with the property name in order to catch the right value. In fact, we don’t have to 😆! The beauty of de-structuring includes also the fact that we can re-name the variable while de-structuring 🚀, e.g:

let {name: fullName} = person;
console.log(fullName); //Maya
console.log(name); //ReferenceError: name is not defined.

Code explanation: define variable fullName and pick its value from property name of person. As simple as it can be!

How about de-structuring array?

Array de-structuring pattern

Let’s look at the following example:

var arr = ["one", "two", "three", "four"];

Similar to Object de-structuring pattern, but instead of curly brackets {}, we will use the square [] brackets syntax on left-hand side assignment to define the value to unpack.

As a result, depend on our needs, we can de-structure partial of array.

var [first, second] = arr; //Only take first 2 elements
var [,,third, four] = arr; //Only take the last 2 elements

Or all of array to distinct variables.

var [one, two, three, four] = arr //Extra all elements

Special note: By adding comma without assigning a variable, we can actually skip element we don’t care about.

However, with this method, you need to be extra careful about the number of commas used and absolutely certain about the location of elements you wish to extract data from. E.g if we want to take only the second and forth elements:

var [,second,,forth] = arr; //second = "two", forth = "four"

And of course, we can perform extract nested values in an element, same as in object de-structuring.

var arr2 = [['one', 'two'], 'three', 'four'];
var [[one, two]] = arr2; //one = 'one', two = 'two'

In addition, Array de-structuring pattern does not only work on Array, but it works also on any Iterable object, such as Map, Set, normal string, etc.

var [a, b] = new Set([1,2]); //a = 1, b = 2;
var [a, b] = new Map([[1,2], [2,3]]); //a= [1,2], b = [2,3]
var [x, y] = "xyz"; //x = "x", y = "y"

Great. Now — Apart from all the cases mentioned above, what are other use cases and how it helps to make our coding life more comfortable?

Uses of De-structuring assignment

Multiple variables declaration & initialization

Assume that we need to declare and initialize more than 3 variables to use in a function context, we can do in normal way, which will require:

let a, b, c, d;
a = 2;
b = 3;
c = { id: 4 };
d = 5;
//Or: let a = 2, b = 3, c = { id: 4 }, d = 5;

Not too bad, but how about this?

let a, b, c, d;
[a, b, c, d] = [2, 3, { id: 4 }, 5];
//Or: let [a, b, c, d] = [2, 3, { id: 4 }, 5];

Go ahead and try with 5, 6, etc… variables. Which method do you prefer?

Destructuring with default values

Extracting data is fun using de-structuring pattern, isn’t it? But one may ask, what if the property/element we are trying to extract value from doesn’t have a value, or even worse, doesn’t exist? We will need to assign some kind of default values to target variables if this happens, right?

Indeed. Normally we will do as follow:

let name = person.name || "default name"; // and so on

With de-structuring, we won’t need the operator || anymore (thank God!)

let {name = "default name"} = obj;

Or

let {name: myFullName = "default name"} = obj;

Code explanation: if the property doesn’t have a valid value or doesn’t exist, the variable name or myFullName will be assigned with default name.

Cool? Wait, there are more.

Combining with (…) rest operator

Yup, cool tools can be combined to make things better. Remember () syntax we discussed? Let’s see how they can work together.

Say we have an input array, and we only want to get the first element value and keep the rest in separate array for other purpose (without modifying the original). Obviously Array.prototype.slice() can help:

let first = arr[0], 
rest = arr.slice(1);

But slice() is not cheap, what if we can use de-structuring with (…)?

let [first, ...rest] = arr;
console.log(first);// "one"
console.log(rest); //["two", "three", "four"]

Way cleaner, isn’t it? But pay attention, rest element can only the last one appeared in de-structuring pattern, or else it will throw an SyntaxError.

Therefore, this method’s downside is that it won’t be applicable to all cases, e.g when we want to de-structure the last element and the rest of arrays.

let [...rest, last] = arr;
//SyntaxError: Rest element must be last element

In this case, slice() will still rule 🚀.

Swapping

Remember the infamous temp we always have to use in the swapping method?

function swap(arr, i, j){
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

Good news! Now with de-structuring, we don’t have to use it anymore.

function swap(arr, i, j){
[arr[i], arr[j]] = [arr[j], arr[i]];
}

🚀 🚀

And last but not least

Destructuring advantage for function

In the first example of buildAnimal(), we have mentioned that we can re-factor parameter list into one object, and each parameter will now be treated as object’s property. In case we need to extract each parameter to distinct variable, we can use object de-structuring pattern:

function buildAnimal(animalData){
let {accessory, animal, color, hairType} = animalData;
console.log(`I'm a ${color} ${animal} with ${accessory} and ${hairType} hair`};
}

This is OK. However, destructuring allows us to directly extract data values from parameter list, instead of additional step of declaring local variables.

function buildAnimal({accessory, animal, color, hairType}}{...}

And we can even define the default values in case the method needs some parameters with default values:

function buildAnimal({accessory = "", animal = "cat", color = 'rainbow', hairType = 'straight'}){...}

When we want to call buildAnimal, no big change needed.

buildAnimal({accessory = "horn", animal = "horse", color = 'purple', hairType = 'curly'}};
//I'm a purple horse with a horn and curly hair.

Special note: Here if we wish to use only default parameter, we will still need to pass empty object as input parameter.

buildAnimal({}); //Works
buildAnimal(); //Not working.

Can we get rid of sending empty object? Certainly yes. We can turn the entire parameter object to be optional with default values by adding ={} to the end. This way allows us to invoke method without passing any parameter at all 👌.

function buildAnimal({accessory = "", animal = "cat", color = 'rainbow', hairType = 'straight'} = {}){...}

Same can be applied to Array parameter

function sum([a = 0, b = 0, c = 0]=[]) { return a + b + c;}
console.log(sum([1, 2, 3])); //6
console.log(sum()); //0

No let, no var, no const needed. Less writing, less code line, less 🐛 trouble!

BUT — Is destructuring assignment really that great to use? Is there any pitfall at all that we need to be aware of? Let’s find out.

Disadvantage of De-structuring assignment

  • We can’t use object destructuring pattern without declaration syntax or round braces () around the ENTIRE statement. Reason? Because Object destructuring pattern starts with curly brackets {}, and in JavaScript, curly brackets {} alone will be considered as block, not object literal.
let a, b;
{a, b} = {a: 1, b: 2}; //Error!!!
({a,b}) = {a: 1, b: 2}; //Error!!
({a, b} = {a: 1, b: 2}); //OK
  • In nested Object destructuring, we need to ensure that the nesting property exists, or else de-structuring will fall.

Conclusion

De-structuring assignment is a good approach for cleaner and shorter code experience. There are more cases you will find it useful than just the ones mentioned above.

However, as always, it is not the absolute correct coding way to apply in every case. Before using, it is recommended to experiment and understand its concept deeply, not just go for it just because it’s cool, new or trendy. After all, think about other developers who will have to read your code one day. Shorter is not always better!

Make the code self-explained without the need of commenting, rather than aiming for shorter code! Don’t you agree 😉?