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);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).
const { name, role = "guest" } = person;const label = name + " (" + role + ")";const person = { name: "Kai", team: "core" };console.log(label);
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
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?
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' ] }
Once it passes, try two variations and predict each before running:
- Spread a discount field into each total. In
lineTotals, add adiscountkey to each returned object ({ name, total: price * qty, discount: 0 }), then predict what the first check does. ThedeepStrictEqualnow fails: each returned object carries an extradiscount: 0key the expected shape doesn't have, so the objects no longer match. An instructive assert failure showing thatdeepStrictEqualcompares the whole shape — an added key breaks equality just as a missing one would. - Destructure
currencyto format the totals. After the checks, pullcurrencyoff the merged settings withconst { 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
= defaultfor fallbacks - Object destructuring unpacks values by key name; use
key: aliasto rename andkey = defaultfor missing properties - Function parameter destructuring makes signatures self-documenting and eliminates repetitive
options.xaccess patterns - Spread (
...collection) expands arrays or objects into a new context — great for copying, merging, and adding properties immutably - Rest (
...nameon 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.
Next lesson
Objects and Prototypes
Master JavaScript objects, the this keyword, and prototypal inheritance. Learn how JS shares behavior between objects under the hood.
22 min