A Re-Introduction To Destructuring Assignment

A Re-Introduction To Destructuring Assignment

A Re-Introduction To Destructuring Assignment

Laurie Barth

If you write JavaScript you’re likely familiar with ES2015 and all the new language standards that were introduced. One such standard that has seen incredible popularity is destructuring assignment. The ability to “dive into” an array or object and reference something inside of it more directly. It usually goes something like this.

const response = {
   status: 200
   data: {}
}

// instead of response.data we get...
const {data} = response //now data references the data object directly


const objectList = [ { key: 'value' }, { key: 'value' }, { key: 'value' } ]

// instead of objectList[0], objectList[1], etc we get...
const [obj, obj1, obj2] = objectList // now each object can be referenced directly

However, destructuring assignment is such a powerful piece of syntax that many developers, even those who have been using it since it was first released, forget some of the things it can do. In this post, we’ll go through five real-world examples for both object and array destructuring, sometimes both! And just for fun, I’ll include a wonky example I came across just the other day.

1. Nested Destructuring

Being able to access a top-level key inside an object, or the first element of an array is powerful, but it’s also somewhat limiting. It only removes one level of complexity and we still end up with a series of dots or [0] references to access what we’re really after.

As it turns out, destructuring can work beyond the top level. And there can be valid reasons for doing so. Take this example of an object response from an HTTP request. We want to go beyond the data object and access just the user. So long as we know the keys we’re looking for, that isn’t a problem.

const response = {
  status: 200,
  data: { 
    user: {
       name: 'Rachel', 
      title: 'Editor in Chief' 
    }, 
    account: {},
    company: 'Smashing Magazine' 
  }
}

const {data: {user}} = response // user is { name: 'Rachel', title: 'Editor in Chief'}

The same can be done with nested arrays. In this case, you don’t need to know the key since there is none. What you need to know is the position of what you’re looking for. You’ll need to provide a reference variable (or comma placeholder) for each element up and until the one you’re looking for (we’ll get to that later). The variable can be named anything since it is not attempting to match a value inside the array.

const smashingContributors = [['rachel', ['writer', 'editor', 'reader']], ['laurie', ['writer', 'reader']]]

const [[rachel, roles]] = smashingContributors
// rachel is 'rachel'
// roles is [ 'writer', 'editor', 'reader' ]

Keep in mind that these features should be used judiciously, as with any tool. Recognize your use case and the audience of your code base. Consider readability and ease of change down the road. For example, if you’re looking to access a subarray only, perhaps a map would be a better fit.

2. Object And Array Destructuring

Objects and arrays are common data structures. So common, in fact, that one often appears inside the other. Beyond nested destructuring, we can access nested properties even if they are in a different type of data structure than the external one we’re accessing.

Take this example of an array inside an object.

const organization = { 
    users: ['rachel', 'laurie', 'eric', 'suzanne'],
    name: 'Smashing Magazine',
    site: 'https://www.smashingmagazine.com/' 
}

const {users:[rachel]} = organization // rachel is 'rachel'

The opposite use case is also valid. An array of objects.

const users = [{name: 'rachel', title: 'editor'}, {name: 'laurie', title: 'contributor'}]

const [{name}] = users // name is 'rachel'

As it turns out, we have a bit of a problem in this example. We can only access the name of the first user; otherwise, we’ll attempt to use ‘name’ to reference two different strings, which is invalid. Our next destructuring scenario should sort this out.

3. Aliases

As we saw in the above example (when we have repeating keys inside different objects that we want to pull out), we can’t do so in the “typical” way. Variable names can’t repeat within the same scope (that’s the simplest way of explaining it, it’s obviously more complicated than that).

const users = [{name: 'rachel', title: 'editor'}, {name: 'laurie', title: 'contributor'}]

const [{name: rachel}, {name: laurie}] = users // rachel is 'rachel' and laurie is 'laurie'

Aliasing is only applicable to objects. That’s because arrays can use any variable name the developer chooses, instead of having to match an existing object key.

4. Default Values

Destructuring often assumes that the value it’s referencing is there, but what if it isn’t? It’s never pleasant to litter code with undefined values. That’s when default values come in handy.

Let’s look at how they work for objects.

const user = {name: 'Luke', organization: 'Acme Publishing'}
const {name='Brian', role='publisher'} = user
// name is Luke
// role is publisher

If the referenced key already has a value, the default is ignored. If the key does not exist in the object, then the default is used.

We can do something similar for arrays.

const roleCounts = [2]
const [editors = 1, contributors = 100] = roleCounts
// editors is 2
// contributors is 100

As with the objects example, if the value exists then the default is ignored. Looking at the above example you may notice that we’re destructuring more elements than exist in the array. What about destructuring fewer elements?

5. Ignoring Values

One of the best parts of destructuring is that it allows you to access values that are part of a larger data structure. This includes isolating those values and ignoring the rest of the content, if you so choose.

We actually saw an example of this earlier, but let’s isolate the concept we’re talking about.

const user = {name: 'Luke', organization: 'Acme Publishing'}
const {name} = user
// name is Luke

In this example, we never destructure organization and that’s perfectly ok. It’s still available for reference inside the user object, like so.

user.organization

For arrays, there are actually two ways to “ignore” elements. In the objects example we’re specifically referencing internal values by using the associated key name. When arrays are destructured, the variable name is assigned by position. Let’s start with ignoring elements at the end of the array.

const roleCounts = [2, 100, 100000]
const [editors, contributors] = roleCounts
// editors is 2
// contributors is 100

We destructure the first and second elements in the array and the rest are irrelevant. But how about later elements? If it’s position based, don’t we have to destructure each element up until we hit the one we want?

As it turns out, we do not. Instead, we use commas to imply the existence of those elements, but without reference variables they’re ignored.

const roleCounts = [2, 100, 100000]
const [, contributors, readers] = roleCounts
// contributors is 100
// readers is 100000

And we can do both at the same time. Skipping elements wherever we want by using the comma placeholder. And again, as with the object example, the “ignored” elements are still available for reference within the roleCounts array.

Wonky Example

The power and versatility of destructuring also means you can do some truly bizarre things. Whether they’ll come in handy or not is hard to say, but worth knowing it’s an option!

One such example is that you can use destructuring to make shallow copies.

const obj = {key: 'value', arr: [1,2,3,4]}
const {arr, arr: copy} = obj
// arr and copy are both [1,2,3,4]

Another thing destructuring can be used for is dereferencing.

const obj = {node: {example: 'thing'}}
const {node, node: {example}} = obj
// node is { example: 'thing' }
// example is 'thing'

As always, readability is of the utmost importance and all of these examples should be used judicially. But knowing all of your options helps you pick the best one.

Conclusion

JavaScript is full of complex objects and arrays. Whether it’s the response from an HTTP request, or static data sets, being able to access the embedded content efficiently is important. Using destructuring assignment is a great way to do that. It not only handles multiple levels of nesting, but it allows for focused access and provides defaults in the case of undefined references.

Even if you’ve used destructuring for years, there are so many details hidden in the spec. I hope that this article acted as a reminder of the tools the language gives you. Next time you’re writing code, maybe one of them will come in handy!

Smashing Editorial (dm, yk, il)

All the New ES2019 Tips and Tricks

The ECMAScript standard has been updated yet again with the addition of new features in ES2019. Now officially available in node, Chrome, Firefox, and Safari you can also use Babel to compile these features to a different version of JavaScript if you need to support an older browser.

Let’s look at what’s new!

Object.fromEntries

In ES2017, we were introduced to Object.entries. This was a function that translated an object into its array representation. Something like this:

let students = {
  amelia: 20,
  beatrice: 22,
  cece: 20,
  deirdre: 19,
  eloise: 21
}

Object.entries(students) 
// [
//  [ 'amelia', 20 ],
//  [ 'beatrice', 22 ],
//  [ 'cece', 20 ],
//  [ 'deirdre', 19 ],
//  [ 'eloise', 21 ]
// ]

This was a wonderful addition because it allowed objects to make use of the numerous functions built into the Array prototype. Things like map, filter, reduce, etc. Unfortunately, it required a somewhat manual process to turn that result back into an object.

let students = {
  amelia: 20,
  beatrice: 22,
  cece: 20,
  deirdre: 19,
  eloise: 21
}

// convert to array in order to make use of .filter() function
let overTwentyOne = Object.entries(students).filter(([name, age]) => {
  return age >= 21
}) // [ [ 'beatrice', 22 ], [ 'eloise', 21 ] ]

// turn multidimensional array back into an object
let DrinkingAgeStudents = {}
for (let [name, age] of DrinkingAgeStudents) {
    DrinkingAgeStudents[name] = age;
}
// { beatrice: 22, eloise: 21 }

Object.fromEntries is designed to remove that loop! It gives you much more concise code that invites you to make use of array prototype methods on objects.

let students = {
  amelia: 20,
  beatrice: 22,
  cece: 20,
  deirdre: 19,
  eloise: 21
}

// convert to array in order to make use of .filter() function
let overTwentyOne = Object.entries(students).filter(([name, age]) => {
  return age >= 21
}) // [ [ 'beatrice', 22 ], [ 'eloise', 21 ] ]

// turn multidimensional array back into an object
let DrinkingAgeStudents = Object.fromEntries(overTwentyOne); 
// { beatrice: 22, eloise: 21 }

It is important to note that arrays and objects are different data structures for a reason. There are certain cases in which switching between the two will cause data loss. The example below of array elements that become duplicate object keys is one of them.

let students = [
  [ 'amelia', 22 ], 
  [ 'beatrice', 22 ], 
  [ 'eloise', 21], 
  [ 'beatrice', 20 ]
]

let studentObj = Object.fromEntries(students); 
// { amelia: 22, beatrice: 20, eloise: 21 }
// dropped first beatrice!

When using these functions make sure to be aware of the potential side effects.

Support for Object.fromEntries

Chrome Firefox Safari Edge
75 67 12.1 No

🔍 We can use your help. Do you have access to testing these and other features in mobile browsers? Leave a comment with your results — we'll check them out and include them in the article.

Array.prototype.flat

Multi-dimensional arrays are a pretty common data structure to come across, especially when retrieving data. The ability to flatten it is necessary. It was always possible, but not exactly pretty.

Let’s take the following example where our map leaves us with a multi-dimensional array that we want to flatten.

let courses = [
  {
    subject: "math",
    numberOfStudents: 3,
    waitlistStudents: 2,
    students: ['Janet', 'Martha', 'Bob', ['Phil', 'Candace']]
  },
  {
    subject: "english",
    numberOfStudents: 2,
    students: ['Wilson', 'Taylor']
  },
  {
    subject: "history",
    numberOfStudents: 4,
    students: ['Edith', 'Jacob', 'Peter', 'Betty']
  }
]

let courseStudents = courses.map(course => course.students)
// [
//   [ 'Janet', 'Martha', 'Bob', [ 'Phil', 'Candace' ] ],
//   [ 'Wilson', 'Taylor' ],
//   [ 'Edith', 'Jacob', 'Peter', 'Betty' ]
// ]

[].concat.apply([], courseStudents) // we're stuck doing something like this

In comes Array.prototype.flat. It takes an optional argument of depth.

let courseStudents = [
  [ 'Janet', 'Martha', 'Bob', [ 'Phil', 'Candace' ] ],
  [ 'Wilson', 'Taylor' ],
  [ 'Edith', 'Jacob', 'Peter', 'Betty' ]
]

let flattenOneLevel = courseStudents.flat(1)
console.log(flattenOneLevel)
// [
//   'Janet',
//   'Martha',
//   'Bob',
//   [ 'Phil', 'Candace' ],
//   'Wilson',
//   'Taylor',
//   'Edith',
//   'Jacob',
//   'Peter',
//   'Betty'
// ]

let flattenTwoLevels = courseStudents.flat(2)
console.log(flattenTwoLevels)
// [
//   'Janet',   'Martha',
//   'Bob',     'Phil',
//   'Candace', 'Wilson',
//   'Taylor',  'Edith',
//   'Jacob',   'Peter',
//   'Betty'
// ]

Note that if no argument is given, the default depth is one. This is incredibly important because in our example that would not fully flatten the array.

let courseStudents = [
  [ 'Janet', 'Martha', 'Bob', [ 'Phil', 'Candace' ] ],
  [ 'Wilson', 'Taylor' ],
  [ 'Edith', 'Jacob', 'Peter', 'Betty' ]
]

let defaultFlattened = courseStudents.flat()
console.log(defaultFlattened)
// [
//   'Janet',
//   'Martha',
//   'Bob',
//   [ 'Phil', 'Candace' ],
//   'Wilson',
//   'Taylor',
//   'Edith',
//   'Jacob',
//   'Peter',
//   'Betty'
// ]

The justification for this decision is that the function is not greedy by default and requires explicit instructions to operate as such. For an unknown depth with the intention of fully flattening the array the argument of Infinity can be used.

let courseStudents = [
  [ 'Janet', 'Martha', 'Bob', [ 'Phil', 'Candace' ] ],
  [ 'Wilson', 'Taylor' ],
  [ 'Edith', 'Jacob', 'Peter', 'Betty' ]
]

let alwaysFlattened = courseStudents.flat(Infinity)
console.log(alwaysFlattened)
// [
//   'Janet',   'Martha',
//   'Bob',     'Phil',
//   'Candace', 'Wilson',
//   'Taylor',  'Edith',
//   'Jacob',   'Peter',
//   'Betty'
// ]

As always, greedy operations should be used judiciously and are likely not a good choice if the depth of the array is truly unknown.

Support for Array.prototype.flat

Chrome Firefox Safari Edge
75 67 12 No
Chrome Android Firefox Android iOS Safari IE Mobile Samsung Internet Android Webview
75 67 12.1 No No 67

Array.prototype.flatMap

With the addition of flat we also got the combined function of Array.prototype.flatMap. We've actually already seen an example of where this would be useful above, but let's look at another one.

What about a situation where we want to insert elements into an array. Prior to the additions of ES2019, what would that look like?

let grades = [78, 62, 80, 64]

let curved = grades.map(grade => [grade, grade + 7])
// [ [ 78, 85 ], [ 62, 69 ], [ 80, 87 ], [ 64, 71 ] ]

let flatMapped = [].concat.apply([], curved) // now flatten, could use flat but that didn't exist before either
// [
//  78, 85, 62, 69,
//  80, 87, 64, 71
// ]

Now that we have Array.prototype.flat we can improve this example slightly.

let grades = [78, 62, 80, 64]

let flatMapped = grades.map(grade => [grade, grade + 7]).flat()
// [
//  78, 85, 62, 69,
//  80, 87, 64, 71
// ]

But still, this is a relatively popular pattern, especially in functional programming. So having it built into the array prototype is great. With flatMap we can do this:

let grades = [78, 62, 80, 64]

let flatMapped = grades.flatMap(grade => [grade, grade + 7]);
// [
//  78, 85, 62, 69,
//  80, 87, 64, 71
// ]

Now, remember that the default argument for Array.prototype.flat is one. And flatMap is the equivalent of combing map and flat with no argument. So flatMap will only flatten one level.

let grades = [78, 62, 80, 64]

let flatMapped = grades.flatMap(grade => [grade, [grade + 7]]);
// [
//   78, [ 85 ],
//   62, [ 69 ],
//   80, [ 87 ],
//   64, [ 71 ]
// ]

Support for Array.prototype.flatMap

Chrome Firefox Safari Edge
75 67 12 No
Chrome Android Firefox Android iOS Safari IE Mobile Samsung Internet Android Webview
75 67 12.1 No No 67

String.trimStart and String.trimEnd

Another nice addition in ES2019 is an alias that makes some string function names more explicit. Previously, String.trimRight and String.trimLeft were available.

let message = "   Welcome to CS 101    "
message.trimRight()
// '   Welcome to CS 101'
message.trimLeft()
// 'Welcome to CS 101   '
message.trimRight().trimLeft()
// 'Welcome to CS 101'

These are great functions, but it was also beneficial to give them names that more aligned with their purpose. Removing starting space and ending space.

let message = "   Welcome to CS 101    "
message.trimEnd()
// '   Welcome to CS 101'
message.trimStart()
// 'Welcome to CS 101   '
message.trimEnd().trimStart()
// 'Welcome to CS 101'

Support for String.trimStart and String.trimEnd

Chrome Firefox Safari Edge
75 67 12 No

Optional catch binding

Another nice feature in ES2019 is making an argument in try-catch blocks optional. Previously, all catch blocks passed in the exception as a parameter. That meant that it was there even when the code inside the catch block ignored it.

try {
  let parsed = JSON.parse(obj)
} catch(e) {
  // ignore e, or use
  console.log(obj)
}

This is no longer the case. If the exception is not used in the catch block, then nothing needs to be passed in at all.

try {
  let parsed = JSON.parse(obj)
} catch {
  console.log(obj)
}

This is a great option if you already know what the error is and are looking for what data triggered it.

Support for Optional Catch Binding

Chrome Firefox Safari Edge
75 67 12 No

Function.toString() changes

ES2019 also brought changes to the way Function.toString() operates. Previously, it stripped white space entirely.

function greeting() {
  const name = 'CSS Tricks'
  console.log(`hello from ${name}`)
}

greeting.toString()
//'function greeting() {\nconst name = \'CSS Tricks\'\nconsole.log(`hello from ${name} //`)\n}'

Now it reflects the true representation of the function in source code.

function greeting() {
  const name = 'CSS Tricks'
  console.log(`hello from ${name}`)
}

greeting.toString()
// 'function greeting() {\n' +
//  "  const name = 'CSS Tricks'\n" +
//  '  console.log(`hello from ${name}`)\n' +
//  '}'

This is mostly an internal change, but I can’t help but think this might also make the life easier of a blogger or two down the line.

Support for Function.toString

Chrome Firefox Safari Edge
75 60 12 - Partial 17 - Partial

And there you have it! The main feature additions to ES2019.

There are also a handful of other additions that you may want to explore. Those include:

Happy JavaScript coding!

The post All the New ES2019 Tips and Tricks appeared first on CSS-Tricks.