Skip to content
JavaScript js modern 4 min read

Destructuring, Spread & Rest

Destructuring, spread (...), and rest (...) are three closely related ES2015+ features that share syntax but solve opposite problems: destructuring pulls values out of arrays and objects, spread expands an iterable or object into a new one, and rest collects leftover values into a fresh array or object. Together they replace a huge amount of boilerplate — manual indexing, Object.assign, apply, and arguments juggling. This page brings them all into one practical reference with idioms you’ll use daily.

Array destructuring

Wrap variable names in [] on the left side; each binds to the element at the same position. Names without a matching element become undefined, and any binding can declare a default with = that applies only when the matched value is undefined.

const [first, second = "n/a", third = "n/a"] = ["a", "b"];
console.log(first, second, third);

// Skip positions with holes, capture the tail with rest
const [, , ...rest] = [10, 20, 30, 40];
console.log(rest);

Output:

a b n/a
[ 30, 40 ]

Because destructuring is positional, it works on any iterable — strings, Sets, and Map entries. A classic idiom is swapping without a temp variable:

let x = 1;
let y = 2;
[x, y] = [y, x];
console.log(x, y);

Output:

2 1

Object destructuring

Object patterns match by key name, not position, so order is irrelevant. You can rename bindings with key: alias, supply defaults, and combine the two. Nested patterns reach into deep structures in a single statement.

const user = {
  id: 7,
  name: "Ada",
  address: { city: "London" },
};

const { name, role = "guest", address: { city } } = user;
console.log(name, role, city);

Output:

Ada guest London

A rest pattern in an object collects the remaining own enumerable properties into a new object — perfect for “everything except” operations.

const { id, ...withoutId } = user;
console.log(withoutId);

Output:

{ name: 'Ada', address: { city: 'London' } }

When destructuring into existing variables (not a const/let declaration), wrap the whole statement in parentheses: ({ a, b } = obj);. A leading { is otherwise parsed as a block.

Spread in arrays and objects

The spread operator expands the contents of an iterable or object into a new literal. It’s the idiomatic way to copy and merge without mutating the source, which matters for predictable state updates in frameworks like React.

const base = [1, 2];
const more = [0, ...base, 3];          // insert anywhere
const copy = [...base];                // shallow clone

const defaults = { theme: "dark", size: "m" };
const merged = { ...defaults, size: "l" }; // later keys win
console.log(more);
console.log(merged);

Output:

[ 0, 1, 2, 3 ]
{ theme: 'dark', size: 'l' }

Object spread (ES2018) only copies own enumerable properties, and like array spread it’s a shallow copy — nested objects are shared by reference. Spread also converts any iterable into an array, replacing Array.from for simple cases:

console.log([...new Set([1, 1, 2, 3])]); // dedupe -> [1, 2, 3]
console.log(Math.max(...[4, 9, 2]));     // spread as arguments -> 9

Rest parameters

Rest gathers an arbitrary number of trailing arguments into a real array — unlike the legacy arguments object, it’s a genuine Array with map, filter, and friends. It must be the last parameter.

function sum(label, ...nums) {
  const total = nums.reduce((acc, n) => acc + n, 0);
  return `${label}: ${total}`;
}
console.log(sum("scores", 10, 20, 30));

Output:

scores: 60

You can combine parameter destructuring with rest to build clean configurable functions:

function createButton({ label, ...attrs } = {}) {
  return { type: "button", label, attrs };
}
console.log(createButton({ label: "OK", disabled: true, id: "ok" }));

Output:

{ type: 'button', label: 'OK', attrs: { disabled: true, id: 'ok' } }

Spread vs rest at a glance

Both use ..., but context decides their meaning: on the right of = or inside a call/literal it spreads (expands); on the left in a pattern or a parameter list it rests (collects).

SyntaxRoleResult
[...arr], {...obj}SpreadExpands into a new array/object
f(...args)SpreadPasses elements as separate arguments
const [a, ...r] = arrRestCollects remaining elements into an array
const { a, ...r } = objRestCollects remaining properties into an object
function f(...args) {}RestCollects all arguments into an array

Best Practices

  • Reach for object destructuring when names matter and array destructuring when position matters.
  • Use spread for immutable copies and merges instead of Object.assign mutation or concat.
  • Remember spread is shallow — clone or restructure nested objects explicitly when you need deep isolation.
  • Prefer rest parameters over the arguments object so you get a real array and arrow-function compatibility.
  • Use object rest (const { x, ...others } = obj) to omit keys cleanly rather than deleting them.
  • Provide destructuring defaults for optional config instead of post-hoc undefined checks.
  • Default a destructured parameter object (= {}) so calling the function with no arguments doesn’t throw.
Last updated June 1, 2026
Was this helpful?