RizTech Academy logo
RizTech Academy
Objects and ArraysLesson 4 of 725 min

Destructuring and spread

Two pieces of syntax have been appearing without explanation since module 2 — the [index, value] in .entries(), and the ... that copies an object. Both are here, and both are everywhere in modern JavaScript.

Pulling values out of an object

The long way:

const order = { customer: 'Priya', plates: 3, paid: true };

const customer = order.customer;
const plates = order.plates;

The short way:

const { customer, plates } = order;

console.log(customer, plates);
Priya 3

The braces on the left are not an object — they are a pattern. "Find a customer property and a plates property, and make variables with those names."

Order does not matter, and you take only what you need.

Renaming

When the property name is wrong for your code, or would collide:

const { customer: name } = order;
console.log(name);
Priya

Read it as "customer, as name". The colon is not a type annotation — a reasonable guess, and wrong.

Defaults

const { rate = 80 } = order;
console.log(rate);
80

order has no rate, so the default applies. And the same trap as function parameters in module 3:

const { discount = 10 } = { discount: null };
console.log(discount);
null

A default fires for undefined only, never for null. Since APIs and databases send null for "no value" constantly, this catches people repeatedly. The next lesson's ?? is the tool for that case.

Nested

const order = { customer: 'Priya', address: { city: 'Pune', pin: '411014' } };
const { address: { city } } = order;

console.log(city);
Pune

Note what that did not create: there is no address variable, only city. The address: part is navigation, not a declaration.

Nesting also inherits the deep-read problem:

const { address: { city } } = { customer: 'Arjun' };
TypeError: Cannot read properties of undefined (reading 'city')

Two levels of destructuring, two levels that must exist. Destructuring null fails too, with a message that names the property:

TypeError: Cannot destructure property 'a' of 'null' as it is null.

Rest

... collects everything you did not name:

const { customer, ...rest } = { customer: 'Priya', plates: 3, paid: true };
console.log(rest);
{ plates: 3, paid: true }

This is the clean way to remove a field: take it out, keep the rest.

Arrays destructure by position

const [first, second] = ['dal', 'rice', 'atta'];
console.log(first, second);
dal rice

Position, not name. Skip with a gap, and defaults work here too:

const [first, , third = 'ghee'] = ['dal', 'rice'];
console.log(first, third);
dal ghee

The empty slot skips 'rice', and third falls back because there is no third item.

This is what .entries() was doing all along:

for (const [index, item] of ['dal', 'rice'].entries()) {
  console.log(index, item);
}
0 dal
1 rice

And the neatest trick in the language — swapping without a temporary:

let a = 1;
let b = 2;
[a, b] = [b, a];

console.log(a, b);
2 1

Destructuring parameters

This is where it earns its keep. Instead of:

function describe(order) {
  return `${order.customer} ordered ${order.plates}`;
}

write:

function describe({ customer, plates = 1 }) {
  return `${customer} ordered ${plates}`;
}

console.log(describe({ customer: 'Arjun' }));
Arjun ordered 1

The signature now documents what the function actually uses. You can read what it needs without reading the body — which is exactly what you want when the object has fifteen fields and the function touches two.

One caution: describe() with no argument at all throws, because it cannot destructure undefined. Give the parameter a default of {} if it is optional.

Spread: the same dots, the other direction

Destructuring takes things apart; spread puts them together.

const items = ['dal', 'rice'];
const more = [...items, 'atta'];

console.log(more);
console.log(items);
[ 'dal', 'rice', 'atta' ]
[ 'dal', 'rice' ]

The original is untouched — this is the non-mutating way to add to a list, and what the last lesson's practice was asking for.

Objects work the same, and later keys win:

console.log({ rate: 80, ...{ rate: 95 } });
console.log({ ...{ rate: 95 }, rate: 80 });
{ rate: 95 }
{ rate: 80 }

That ordering is the whole technique for applying updates:

const order = { customer: 'Priya', plates: 3, paid: false };
const updated = { ...order, paid: true };

console.log(updated);
console.log(order.paid);
{ customer: 'Priya', plates: 3, paid: true }
false

A new object with one field changed, original intact. This is how React, Redux and every state library expect you to update data, and the habit is worth building now.

Spread also turns an array into arguments:

console.log(Math.max(...[3, 9, 2]));
9

Math.max takes separate numbers, not an array, and the dots do the unpacking.

The shallow-copy trap, again

Spread copies one level. From the objects lesson, but it matters enough to repeat:

const base = { customer: 'Priya', address: { city: 'Pune' } };
const clone = { ...base };

clone.address.city = 'Mumbai';
console.log(base.address.city);
Mumbai

So { ...order, paid: true } is safe — paid is a plain value. But { ...order } followed by clone.address.city = ... is not. To update something nested, spread at each level:

const updated = {
  ...order,
  address: { ...order.address, city: 'Mumbai' },
};

Verbose, and honest about what it is doing. For anything deeper than two levels, structuredClone or a library is kinder.

Check your work

const { customer, plates } = order creates two variables from the matching properties. Order in the pattern does not matter.

const { customer: name } creates name, not customer. Read it as "customer, as name".

const { rate = 80 } = order gives 80 when order has no rate — but const { discount = 10 } = { discount: null } gives null. Defaults fire for undefined only.

const { address: { city } } = order creates city and not address. If address is missing it throws Cannot read properties of undefined (reading 'city').

const { customer, ...rest } = order puts everything else in rest. The cleanest way to drop a field.

const [first, , third = 'ghee'] = ['dal', 'rice'] gives dal and ghee — the gap skips a position, and the default fills a missing one.

[a, b] = [b, a] swaps them.

function describe({ customer, plates = 1 }) called with { customer: 'Arjun' } gives Arjun ordered 1. Called with no argument at all, it throws — use = {} on the parameter if it is optional.

{ rate: 80, ...{ rate: 95 } } is { rate: 95 }; reversed it is { rate: 80 }. Later keys win, which is what makes { ...order, paid: true } an update.

{ ...order, paid: true } leaves order unchanged.

Spread is shallow. { ...base } then changing clone.address.city also changes base. Spread each level, or structuredClone.

Math.max(...[3, 9, 2]) is 9.

moveCity needs a spread at every level it changes:

function moveCity(order, city) {
  return { ...order, address: { ...order.address, city } };
}

{ ...order, address: { ...order.address, city } } builds a new order and a new address. The single-spread version, { ...order } followed by copy.address.city = city, shares the original address object — so changing the copy's pincode afterwards would also change the original's. Test it by mutating the result and then reading the original: with the version above, the original still reads Pune and 411014.

Practice

  1. Destructure customer and plates out of an order in one line.
  2. Rename customer to name while destructuring.
  3. Give rate a default and confirm it applies. Then set the property to null and watch the default stand aside.
  4. Destructure a nested city. Then confirm no address variable exists.
  5. Destructure a nested property from an object missing the middle level, and read the error.
  6. Use rest to pull paid out and keep everything else.
  7. Destructure the first and third items of an array, skipping the second.
  8. Swap two variables with array destructuring.
  9. Rewrite a function to destructure its parameter with a default. Then call it with no argument and read the error.
  10. Use spread to add an item to a list and to update one field of an object, proving in both cases that the original is unchanged.
  11. Harder. Given { customer: 'Priya', address: { city: 'Pune', pin: '411014' } }, write moveCity(order, city) returning a new order with a new city, where neither the original order nor its original address is modified. Prove it by changing the result afterwards and checking the original. A single spread will look right and fail this test — that is the exercise.

Next: optional chaining — the fix for the Cannot read properties of undefined error this lesson has now caused twice.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship