JavaScript Spread and Destructuring: The Modern Way to Compose and Unpack Data
Spread (...) and destructuring (const [a, b] = arr) are two of the biggest readability wins of modern JavaScript. They have nothing to do with each other syntactically, but they pair so well in everyday code that it makes sense to learn them together.
This lesson covers both: how spread works in arrays, objects, and function calls; how destructuring works for arrays and objects; defaults, renaming, nested patterns, and the rest pattern — the dual of spread.
Spread: the ... that pulls things apart #
The spread operator unpacks an iterable (array, string, Map, Set) or an object's own enumerable properties.
Array spread #
const a = [1, 2, 3];
const b = [0, ...a, 4]; // [0, 1, 2, 3, 4]
const combined = [...a, ...[10, 20]]; // [1, 2, 3, 10, 20]
const clone = [...a]; // shallow copy
Spread works on anything iterable — strings, Maps, Sets, NodeLists, generators.
[...'hello']; // ['h', 'e', 'l', 'l', 'o']
[...new Set([1, 1, 2, 2, 3])]; // [1, 2, 3] — deduplication idiom
[...document.querySelectorAll('p')]; // NodeList → array
De-duping with new Set + spread is the most idiomatic one-liner for unique values.
Object spread #
const user = { name: 'Ada', age: 36 };
const withEmail = { ...user, email: 'ada@x.com' }; // adds a field
const updated = { ...user, age: 37 }; // updates a field
const withoutAge = (({ age, ...rest }) => rest)(user); // removes a field (clever)
Object spread is the foundation of immutable updates. Order matters: later keys override earlier ones.
const a = { x: 1, y: 2 };
const b = { y: 99, z: 3 };
const merged = { ...a, ...b }; // { x: 1, y: 99, z: 3 }
const byFirst = { ...b, ...a }; // { y: 2, z: 3, x: 1 }
This is also the pattern for defaults — put the user-provided values second:
const settings = { ...DEFAULTS, ...userPrefs };
Spread in function calls #
Math.max(...nums); // pass array as separate arguments
fn(a, b, ...rest); // mix fixed and spread arguments
fn(...new Set(items)); // de-dupe before calling
Replaces the older Math.max.apply(null, nums) pattern.
Spread is shallow #
Nested objects and arrays are still shared by reference.
const a = { x: { n: 1 } };
const b = { ...a };
b.x.n = 99;
console.log(a.x.n); // 99 — nested object shared!
For deep clones use structuredClone(value) (modern, since 2022). Spread is for the top level only.
Destructuring: the inverse of spread #
Destructuring extracts values from arrays or objects into named variables.
Array destructuring #
const [a, b, c] = [10, 20, 30];
console.log(a, b, c); // 10 20 30
Positional. Order matters. Skip with commas:
const [, , third] = [10, 20, 30];
console.log(third); // 30
Default values for missing elements:
const [a = 1, b = 2, c = 3] = [10];
console.log(a, b, c); // 10 2 3
Defaults only kick in for undefined — null does NOT trigger the default:
const [x = 5] = [null];
console.log(x); // null
Object destructuring #
const { name, age } = { name: 'Ada', age: 36, email: 'ada@x.com' };
console.log(name, age); // 'Ada' 36
Name-based. Extra properties are ignored. Missing properties are undefined.
Rename while destructuring with ::
const { name: userName, age: userAge } = user;
console.log(userName, userAge);
The name: userName pattern reads as "extract name, call it userName."
Defaults:
const { theme = 'dark', lang = 'en' } = prefs;
Combine renaming and defaults:
const { name: userName = 'Anonymous' } = user;
Destructuring in function parameters #
The most useful place to destructure. Replace options object boilerplate:
// Before
function createUser(opts) {
const name = opts.name;
const age = opts.age || 0;
const active = opts.active ?? true;
// ...
}
// After — clean parameter destructuring with defaults
function createUser({ name, age = 0, active = true }) {
// ...
}
The parameter name effectively disappears — the function declares what fields it needs right in its signature.
For optional configs, default the whole object too:
function connect({ host = 'localhost', port = 5432 } = {}) {
// ...
}
connect(); // uses defaults
connect({ host: 'db.x.com' }); // partial override
Without the = {}, calling connect() would try to destructure undefined and throw.
Nested destructuring #
Reaches deep into structures:
const response = {
data: { user: { name: 'Ada', address: { city: 'London' } } },
};
const {
data: {
user: {
name,
address: { city },
},
},
} = response;
console.log(name, city); // 'Ada' 'London'
Clean for predictable shapes, opaque for variable ones. For deeply optional data, optional chaining (?.) is often clearer.
The rest pattern: spread's dual #
Inside a destructuring, ... collects "everything else" into one variable.
Array rest #
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first, second, rest); // 1 2 [3, 4, 5]
rest must be the last element.
Object rest #
const { name, ...rest } = { name: 'Ada', age: 36, email: 'ada@x.com' };
console.log(name, rest); // 'Ada' { age: 36, email: 'ada@x.com' }
Idiomatic for stripping a field:
const { password, ...safeUser } = user; // safeUser without password
And for splitting props in React/Angular components:
function Button({ variant, children, ...rest }) {
return <button {...rest}>{children}</button>;
}
Rest in function parameters #
function sum(...nums) {
return nums.reduce((s, n) => s + n, 0);
}
sum(1, 2, 3); // 6
This is how modern variadic functions are written. Replaces the old arguments object — and unlike arguments, nums is a real array.
Common patterns #
Swap two variables #
let a = 1, b = 2;
[a, b] = [b, a];
No temp variable needed.
Return multiple values #
function getRange(arr) {
return [Math.min(...arr), Math.max(...arr)];
}
const [min, max] = getRange([3, 1, 4]);
Or with named returns via objects:
function getStats(arr) {
return { min: Math.min(...arr), max: Math.max(...arr) };
}
const { min, max } = getStats([3, 1, 4]);
Object form is friendlier when more values are added later.
Pull a few props out, keep the rest #
const { metadata, ...content } = response;
Ubiquitous in React/Angular/data-transformation code.
Tagged tuples from Object.entries #
for (const [key, value] of Object.entries(obj)) {
console.log(`${key}: ${value}`);
}
The iteration pattern from Lesson 3.1 is just array destructuring inside a for...of.
Default-from-existing #
const { x = arr.length } = someConfig;
Default values are evaluated lazily — arr.length only runs if x is missing.
A few gotchas #
Destructuring undefined throws #
const { x } = undefined;
// TypeError: Cannot destructure property 'x' of 'undefined' as it is undefined.
Use defaults to guard:
const { x } = obj ?? {}; // safe even if obj is null/undefined
Or defaultize a parameter (as shown earlier).
Spread an iterable, not just an array #
... on a non-iterable throws:
const obj = { a: 1, b: 2 };
const arr = [...obj]; // TypeError: obj is not iterable
For object → array of entries, use Object.entries(obj).
... for object spread is NOT iteration #
const a = { x: 1 };
const b = { ...a, y: 2 };
Object spread copies own enumerable properties — not via iteration. So a class instance's methods (which live on the prototype) don't come along:
class User { greet() {} }
const u = new User();
const copy = { ...u };
typeof copy.greet; // 'undefined' — methods stayed on the prototype
Use Object.assign(Object.create(Object.getPrototypeOf(u)), u) or structuredClone if you need the prototype too.
A summary #
...arrunpacks an iterable into a new array, function args, or constructor.{...obj}copies own enumerable properties. Shallow.[a, b] = arrextracts positionally.{name} = objextracts by name.= defaultvalues inside destructuring activate only onundefined....restcollects "everything else" — must be last.- Spread is shallow; use
structuredClonefor deep copies. - Defaultize containers (
= {},= []) when destructuring possibly-undefined inputs.
What's next #
Lesson 3.4 covers property descriptors — Object.defineProperty, the four flags (value, writable, enumerable, configurable), getters and setters, and how Object.freeze actually works underneath. It's the bridge between the surface-level object syntax we just covered and the deeper mechanics in Module 4.
Try it yourself #
The rest pattern combined with object spread is the most useful idiom of all. Predict the output:
const user = { name: 'Ada', age: 36, password: 's3cret' };
const { password, ...safe } = user;
console.log(safe);js_sandboxOutput: { name: 'Ada', age: 36 }.The rest pattern collected everything except
password. The original user is unchanged — destructuring doesn’t mutate.That’s the modern “strip sensitive fields” idiom. One line, immutable, no
delete, no lodash.omit.That one pattern alone replaces a lot of utility code that older codebases imported entire libraries for.
Up next in JavaScript
More from this topic
Enjoyed this article?
Get new JavaScript tutorials delivered. No spam — just code-first articles when they ship.


