loading...

let { [key]: id = 0, ...rest } = obj - Destructuring to the limit

michi profile image Michael Z ・4 min read

I recently ran into a problem where I needed the following piece of JavaScript

let { [key]: id, ...rest } = obj

So in this post I want to explain what this is doing and how it works.

Spoiler alert: I ended up not implementing it this way because of its obscurity, but it is interesting nontheless

How I ran into this problem?

Imagine we have the following array

const users = [ 
    { name: 'Michael', group: 1 },
    { name: 'Lukas', group: 1 },
    { name: 'Travis', group: 2 },
]

and we want to group it by the key group and turn it into a hashMap which would look like this

{
    '1': [
        { name: 'Michael' },
        { name: 'Lukas' },
    ],
    '2': [
        { name: 'Travis' },
    ]
}

Notice how we remove the group from the user object.

We can achieve this using

users.reduce((result, user) => {
  const { group, ...userData } = user
  result[group] = result[group] || []
  result[group].push(userData)

  return result
}, {})

If you are unfamiliar with reduce check out my article on array methods.

My end goal was to make this function dynamic, right now the group key is all hardcoded and not computed. But before we look at that, let's check out const { group, ...userData } = user since it is exactly the expression I want to talk about, just not dynamic.

Destructuring

We know that each user has the keys group and name, so in ES6 we can use a feature called destructuring to get individual values from an object.

For example

const { group } = user

would be the same as writing

const group = user.group

and

const { group, name } = user

would likewise be the same as

const group = user.group
const name = user.name

Rest

Now there is one more complexity in our initial line: const { group, ...userData } = user.

...userData is taking all key value pairs except for group and shallow copies them into a new constant named userData. In this case the variable userData would be an object with only the name property.

Don't confuse the rest parameter with spreading. Spreading would be kind of the opposite.

const location = { country: 'Japan', city: 'Tokyo' }

const newLocation = { ...location, zipcode: 123456 }

This takes the location object and spreads it out, so newLocation will be a completely new object that has all the properties from location as well as zipcode.

When is something rest and when is something spread? It all depends on which side the assignment is. If something is on the left side of the assignment it would be rest, if something is on the right side of the assignment it would be spread.

You can also use the rest parameter for functions.

class BaseArray extends Array {
    constructor(...values) { // rest

        super(...values) // spread

    }
}

With that out of the way, let's look at the dynamic solution.

function groupBy(array, key) {
    return array.reduce((result, item) => {
        const { [key]: id, ...rest } = item
        result[id] = result[id] || new []

        result[id].push(rest);

        return result;
    }, {})
}

Now what the heck is const { [key]: id, ...rest } = item?

We already know what ...rest means, so we can ignore that for now. Before explaining [key]: id, let's look at a simpler example.

Assigning new variable names

Remember this?

const user = { group: 1 }
const { group } = user
console.log(group) //? 1

What if we wanted to apply the value of group to a different variable name? We can do it like this

const user = { group: 1 }
const { group: id } = user
console.log(id) //? 1

This takes the value of group and puts it inside the variable id.

This is actually really useful because sometimes keys would be invalid as variable names.

const foo = { 'fizz-buzz': true }
const { 'fizz-buzz': fizzBuzz } = foo

Now how do we remember this syntax? It's actually quite simple. You just have to think about the side of the assignment again.
When we create objects we have exactly the same syntax

const id = 1
const user = {
    group: id
}

So if the object is on the right side of the assignment, we give the object user a property group that holds the variable id.

If it is on the left side of the assignment it would be the opposite.

const { group: id } = user

We take the value of property group and put it inside the variable id.

Finally, computed object properties names

So the only thing left to explain is [key].

We can use this to access a computed property name, in our case the variable key holds the value group.

Again, nothing new here.

How do you add computed keys when creating objects?

Using the same syntax, just that it's on the right side of the assignment!

const key = 'group'
const id = 1

const user = {
    [key]: id
}

But if we would just write let { [key] } = obj under what name are we supposed to access this variable then? Well, we can't, so like with fizz-buzz we need to assign it to a new variable using :. This combination ultimately creates [key]: id.

So that's that, how can we make it even more obscure? By applying a default value to the id!

Usually it would look like this

const user = { group: 1 }

const { group = 0, createdAt = null} = user

Using a computed property it becomes

let { [key]: id = 0, ...rest } = obj

References

Posted on by:

Discussion

markdown guide