Skip to lesson

learningjavascript.org / basics / 17-destructuring-and-spread · lesson 7 of 25

TL;DR

Learn JavaScript destructuring and the spread operator. Unpack arrays and objects to write cleaner, more expressive modern JavaScript.

Key concepts

  • JavaScript destructuring
  • spread operator JavaScript
  • rest parameters JS
  • JS destructuring tutorial

Destructuring and Spread

Modern JavaScript gives you two closely related features that change how you work with arrays and objects: destructuring and the spread operator. They show up constantly in real codebases — in React components, API responses, function arguments, and data transformations.

Once you understand them, you will wonder how you lived without them.

Array Destructuring

Destructuring lets you pull values out of arrays and objects into named variables. Instead of accessing items by index, you describe the shape you want and JavaScript fills in the values.

const colors = ["red", "green", "blue"];

// Old way
const first = colors[0];
const second = colors[1];

// Destructuring
const [primary, secondary, tertiary] = colors;

console.log(primary);   // "red"
console.log(secondary); // "green"
console.log(tertiary);  // "blue"

// Skip elements with commas
const [, , last] = colors;
console.log(last); // "blue"

// Default values when the array is shorter than expected
const [a, b, c, d = "yellow"] = colors;
console.log(d); // "yellow"

This is particularly useful when a function returns multiple values packaged as an array. Promise.all, coordinate pairs, and many utility functions follow this pattern.

Object Destructuring

Object destructuring works the same way but uses property names instead of positions. You write the keys you want inside curly braces, and JavaScript extracts them.

const user = {
  name: "Ada Lovelace",
  age: 36,
  role: "engineer",
  location: "London"
};

// Pull out specific properties
const { name, role } = user;
console.log(name); // "Ada Lovelace"
console.log(role); // "engineer"

// Rename while destructuring
const { name: fullName, age: yearsOld } = user;
console.log(fullName);  // "Ada Lovelace"
console.log(yearsOld);  // 36

// Default values
const { name: username, verified = false } = user;
console.log(verified); // false — not in the object, so default applies

// Nested destructuring
const config = {
  server: {
    host: "localhost",
    port: 3000
  }
};

const { server: { host, port } } = config;
console.log(host); // "localhost"
console.log(port); // 3000

Renaming is handy when variable names would collide, or when the original key names are not descriptive in context.

Predict

A default in destructuring only kicks in under one specific condition. Predict all three lines before running — pay attention to which values are present in the object.

const settings = { timeout: 0, retries: null };

const { timeout = 30, retries = 3, cache = true } = settings;

console.log(timeout);
console.log(retries);
console.log(cache);
Continue learning

Destructuring In Function Parameters

One of the most common uses of destructuring is in function signatures. Instead of receiving an object and then accessing its properties manually, you destructure directly in the parameter list.

// Without destructuring
function greet(user) {
  return `Hello, ${user.name}! You are ${user.age} years old.`;
}

// With destructuring in parameters
function greetUser({ name, age, role = "member" }) {
  return `Hello, ${name}! You are ${age} years old and a ${role}.`;
}

const person = { name: "Grace Hopper", age: 85, role: "admiral" };
console.log(greetUser(person));
// "Hello, Grace Hopper! You are 85 years old and a admiral."

// Works with inline objects too
console.log(greetUser({ name: "Alan", age: 41 }));
// "Hello, Alan! You are 41 years old and a member."

This pattern is everywhere in React — component props are almost always destructured in the function signature.

Use the move up and move down controls to put the code lines in dependency order.

Four JavaScript statements to reorder

Arrange the code

Reorder the lines to destructure a name and default role, build a label, and log Kai (guest).

  1. const { name, role = "guest" } = person;
  2. const label = name + " (" + role + ")";
  3. const person = { name: "Kai", team: "core" };
  4. console.log(label);
Continue learning

The Spread Operator

The spread operator (...) does the opposite of destructuring. Instead of unpacking a collection into variables, it expands a collection into individual elements. It works with both arrays and objects.

// Spread with arrays
const fruits = ["apple", "banana"];
const moreFruits = ["cherry", "date"];

const allFruits = [...fruits, ...moreFruits];
console.log(allFruits);
// ["apple", "banana", "cherry", "date"]

// Copy an array without mutating the original
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3] — unchanged
console.log(copy);     // [1, 2, 3, 4]

// Spread with objects
const defaults = { theme: "light", fontSize: 14, language: "en" };
const userPrefs = { fontSize: 18, language: "fr" };

// Merge objects — later keys override earlier ones
const finalSettings = { ...defaults, ...userPrefs };
console.log(finalSettings);
// { theme: "light", fontSize: 18, language: "fr" }

// Add new properties while spreading
const updatedUser = { ...defaults, showNotifications: true };
console.log(updatedUser);
// { theme: "light", fontSize: 14, language: "en", showNotifications: true }

Object spread is how you update state immutably in React and other frameworks — create a new object with existing values plus your changes, never mutating the original.

Debug

This is supposed to make an independent copy of original, change only the copy's theme to 'dark', and leave the original as 'light'. The guard at the bottom throws because the original got mutated anyway. Predict why, then fix it so the program runs to completion and logs 'Original preserved: light'.

const original = {
name: "Ada",
settings: { theme: "light", fontSize: 14 },
};

// Intent: an independent copy we can edit without touching the original.
const copy = { ...original };
copy.settings.theme = "dark";

if (original.settings.theme !== "light") {
throw new Error(
  "Original was mutated! theme is now: " + original.settings.theme
);
}
console.log("Original preserved:", original.settings.theme);

Expected output: Original preserved: light

Continue learning

The Rest Operator

The same ... syntax used in a different position becomes the rest operator. Instead of spreading values out, it collects the remaining ones.

In arrays, rest collects everything after the named variables. In objects, it collects all properties not explicitly destructured.

// Rest in array destructuring
const [head, ...tail] = [10, 20, 30, 40, 50];
console.log(head); // 10
console.log(tail); // [20, 30, 40, 50]

// Rest in object destructuring
const { name, age, ...rest } = {
  name: "Linus",
  age: 54,
  country: "Finland",
  language: "C"
};
console.log(name); // "Linus"
console.log(rest); // { country: "Finland", language: "C" }

// Rest in function parameters — collects all arguments
function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}

console.log(sum(1, 2, 3));        // 6
console.log(sum(10, 20, 30, 40)); // 100

Rest parameters are the modern replacement for the old arguments object — arguments still works, but rest gives you a real array, and it is the only option inside arrow functions, which have no arguments of their own.

Recall

Without scrolling up: you have an array of user objects [{ name, age }, ...] and you want just an array of their names. You reach for map from 16-array-methods, but you only need the name from each object. Which callback pulls it out most directly, and why does it work?

Continue learning

Try It Yourself

Reading about destructuring and spread is not the same as reaching for them. This is a build task: a small program that reports its own pass/fail. You are given a cart of line items — an array of objects — plus the store's defaults, and three empty functions to finish. Run it as-is and it fails immediately, telling you which function is still missing. Implement each one until every check passes and it prints All checks passed.

The three functions reuse exactly what this lesson taught: destructuring fields (with a default) out of each object in a map, spread-merging two objects so the later one wins, and splitting the first element off an array with a rest pattern. The starter already has the data, the stubs, and the checks — you write only the logic inside each function. Nothing above spells out all three answers, so you will have to assemble them yourself.

Build

Finish the build. Three functions are stubbed out and the checks below them fail until each one returns the right value. Run it as-is to see which check fails first, decide what that function is missing, then implement the three functions until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down.

const assert = require('assert');

// The data: a cart of line items from an API. Do NOT change this array.
const items = [
{ name: "Notebook", price: 4.99, qty: 3 },
{ name: "Pen Set", price: 9.99, qty: 1 },
{ name: "Desk Lamp", price: 34.99, qty: 2 },
];

// The store's defaults. Do NOT change this object.
const defaults = { currency: "USD", shipping: 5, gift: false };

// TODO 1: destructure name, price, and qty from each item and return a NEW
// array of line-total objects. Default qty to 1 if an item has no qty.
//   lineTotals(items) -> [ { name: "Notebook", total: 14.97 }, ... ]
function lineTotals(items) {
// your code here
}

// TODO 2: merge the defaults with the given overrides using object spread, so
// overrides win. Do NOT mutate defaults.
//   withOverrides(defaults, { shipping: 0 }) -> { currency: "USD", shipping: 0, gift: false }
function withOverrides(defaults, overrides) {
// your code here
}

// TODO 3: split the first item off the array with array destructuring and rest.
// Return an object with the first item's name and an array of the rest's names.
//   featured(items) -> { first: "Notebook", rest: ["Pen Set", "Desk Lamp"] }
function featured(items) {
// your code here
}

// --- Build checks: these must all pass. Do not edit below this line. ---
assert.deepStrictEqual(
lineTotals(items),
[
	{ name: "Notebook", total: 14.97 },
	{ name: "Pen Set", total: 9.99 },
	{ name: "Desk Lamp", total: 69.98 },
],
"lineTotals(items) should destructure each item and return name + price*qty",
);
assert.deepStrictEqual(
withOverrides(defaults, { shipping: 0 }),
{ currency: "USD", shipping: 0, gift: false },
"withOverrides should spread-merge overrides over defaults, overrides winning",
);
assert.deepStrictEqual(
defaults,
{ currency: "USD", shipping: 5, gift: false },
"withOverrides must NOT mutate the original defaults object",
);
assert.deepStrictEqual(
featured(items),
{ first: "Notebook", rest: ["Pen Set", "Desk Lamp"] },
"featured should split the first item off with rest and return its name plus the others' names",
);

console.log("All checks passed.");
console.log("Line totals:", lineTotals(items));
console.log("With overrides:", withOverrides(defaults, { shipping: 0 }));
console.log("Featured:", featured(items));

Expected output: All checks passed. Line totals: [ { name: 'Notebook', total: 14.97 }, { name: 'Pen Set', total: 9.99 }, { name: 'Desk Lamp', total: 69.98 } ] With overrides: { currency: 'USD', shipping: 0, gift: false } Featured: { first: 'Notebook', rest: [ 'Pen Set', 'Desk Lamp' ] }

Continue learning

Once it passes, try two variations and predict each before running:

  1. Spread a discount field into each total. In lineTotals, add a discount key to each returned object ({ name, total: price * qty, discount: 0 }), then predict what the first check does. The deepStrictEqual now fails: each returned object carries an extra discount: 0 key the expected shape doesn't have, so the objects no longer match. An instructive assert failure showing that deepStrictEqual compares the whole shape — an added key breaks equality just as a missing one would.
  2. Destructure currency to format the totals. After the checks, pull currency off the merged settings with const { currency } = withOverrides(defaults, { shipping: 0 }), then map the line totals to labelled strings and log them. You get [ "Notebook: USD 14.97", "Pen Set: USD 9.99", "Desk Lamp: USD 69.98" ] — object destructuring lifting one key out of the spread-merged object. This changes the echoed output, not any check.

Key Takeaways

  • Array destructuring unpacks values by position; use commas to skip elements and = default for fallbacks
  • Object destructuring unpacks values by key name; use key: alias to rename and key = default for missing properties
  • Function parameter destructuring makes signatures self-documenting and eliminates repetitive options.x access patterns
  • Spread (...collection) expands arrays or objects into a new context — great for copying, merging, and adding properties immutably
  • Rest (...name on the receiving end) collects remaining elements — works in destructuring assignments and function parameters
  • Spread and rest use the same ... syntax but mean opposite things depending on where they appear
  • Object spread copies only the top level — nested objects are still shared references, not deep clones

Pro Tip: When merging objects with spread, order matters — later properties overwrite earlier ones. Put your defaults first and your overrides last: { ...defaults, ...userOptions }. This is the idiomatic way to write configurable functions that accept optional settings without requiring every key to be provided.

Next Steps

With destructuring and spread in your toolkit, you are ready to explore objects and prototypes — how JavaScript builds objects with methods, the this keyword, and the prototype chain that powers inheritance.

Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.