Destructuring, Spread, and Rest

Pulling values out of objects and arrays, and copying them without mutation.

A show object holds fields you read over and over. Destructuring gives each field its own name, and three dots copy the rest.

Destructuring objects

Destructuring unpacks fields into variables in one step. The name on the left has to match the field name.

const show = { title: "Severance", year: 2022, rating: 8.7, network: null };

const { title, year } = show;

console.log("title:", title);
console.log("year: ", year);

// Rename with a colon, and give a default for a missing field.
const { rating: score, seasons = 1, network = "Unknown" } = show;

console.log("score:  ", score);
console.log("seasons:", seasons);
console.log("network:", network);

seasons is not in the object, so the default 1 is used. network prints null, not "Unknown". A default only fills in undefined.

Destructuring in parameters

You can unpack a parameter in the function's own brackets. React components read their props this way.

function label({ title, year }) {
  return title + " (" + year + ")";
}

const show = { title: "Severance", year: 2022 };

console.log("label:", label(show));

// No argument at all: there is nothing to unpack.
try {
  label();
} catch (error) {
  console.log("no argument:", error.constructor.name);
}

The first call prints label: Severance (2022). The second throws a TypeError, because you cannot read title off undefined. Writing function label({ title, year } = {}) gives it an empty object to unpack.

Destructuring arrays

Arrays unpack by position. You choose the names, so a name never picks the item you meant.

const shows = [
  { title: "Severance", year: 2022 },
  { title: "Andor", year: 2022 },
];

const [first, second] = shows;

console.log("first: ", first.title);
console.log("second:", second.title);

// The name comes from the position, not from the data.
const [andor, severance] = shows;

console.log("named andor:", andor.title);

The last line prints named andor: Severance. Position one is Severance, whatever you call it.

Rest and spread

Both are three dots. Rest collects leftovers on the left of an =. Spread expands a value on the right.

Rest collects

Functions and Parameters used ...rest to collect extra arguments. The same dots work in a pattern.

const show = { title: "Severance", year: 2022, rating: 8.7 };

const { title, ...rest } = show;

console.log("title:", title);
console.log("rest: ", rest);

rest is a new object with the fields you did not name.

Spread expands

Spread copies every field into a new object. A field written after the spread replaces the copied one.

const show = { title: "Severance", year: 2022, watched: false };

// Copy every field, then change one. A later field wins.
const seen = { ...show, watched: true };

console.log("original:", show.watched);
console.log("copy:    ", seen.watched);

// Spread last, and the old value overwrites yours.
const wrong = { watched: true, ...show };

console.log("wrong order:", wrong.watched);

seen.watched is true and the original is untouched. wrong.watched is false, because ...show ran after it and put the old value back.

Arrays spread the same way.

const shows = [{ title: "Severance" }, { title: "Andor" }];

const added = [...shows, { title: "Shogun" }];

console.log("original length:", shows.length);
console.log("copy length:    ", added.length);
console.log("last title:     ", added[2].title);

shows still has two items. added is a new array with three.

Copying instead of mutating

An object variable holds a reference, not the object itself. Two names can point at one object, so changing it through either name changes it for both.

const show = { title: "Severance", year: 2022 };
const other = { title: "Andor", year: 2022 };

// Not a copy: both names point at one object.
const alias = show;
alias.year = 2020;

// A copy: spread builds a new object to change.
const copy = { ...other, year: 2020 };

console.log("show year: ", show.year);
console.log("other year:", other.year);
console.log("copy year: ", copy.year);

show.year is now 2020, even though you only wrote to alias. other.year is still 2022, because copy is a different object.

This matters later. React works out what changed by comparing objects, so it misses a change you make in place.

One level deep

Spread copies the top level only. A nested object is copied as a reference, so both copies still share it.

const show = { title: "Severance", rating: { score: 8.7 } };

// Copies the title, but shares the same rating object.
const shallow = { ...show };

// Copies at both levels, so the rating is its own object.
const deep = { ...show, rating: { ...show.rating, score: 9 } };

shallow.rating.score = 1;

console.log("original score:", show.rating.score);
console.log("shallow score: ", shallow.rating.score);
console.log("deep score:    ", deep.rating.score);

Writing to shallow.rating changed show.rating too: both print 1. deep kept its own rating, so it prints 9. Copy at every level you change.

Common mistakes

  • Expecting a default to fill in null. A default only replaces undefined.
  • Spreading last. { watched: true, ...show } puts the old value back; spread first, then override.
  • Trusting a shallow copy. { ...show } shares every nested object.

Exercise

addGenre should return a new show with the genre added, and leave the show it was given alone. Right now both lines print the same list.

The output should read:

original: Drama
updated:  Drama,Thriller

Hint: spread the show into a new object, and the old genres into a new array.

function addGenre(show, genre) {
  show.genres.push(genre);
  return show;
}

const original = { title: "Severance", genres: ["Drama"] };
const updated = addGenre(original, "Thriller");

console.log("original: " + original.genres);
console.log("updated:  " + updated.genres);