JavaScript Intermediate #4 Destructuring and spread/rest in Depth

6 min read

In Basics #5 Objects and Arrays you saw destructuring and spread lightly. This post takes those tools deeper — into patterns you meet often in practice.

Object destructuring — basics + variations #

basics + rename + default
const user = { id: 'u1', name: 'Curtis', age: 30 };

// basic
const { name, age } = user;

// rename
const { name: userName } = user;

// default
const { email = 'none' } = user;

// rename + default
const { phone: userPhone = 'unset' } = user;

The last pattern is confusing at first — receive the phone key as userPhone, defaulting to 'unset' if missing. Common in functions taking option objects.

Unpacking nested objects #

nested destructuring
const response = {
  data: {
    user: {
      id: 'u1',
      profile: {
        name: 'Curtis',
        age: 30,
      },
    },
  },
};

const {
  data: {
    user: {
      profile: { name, age },
    },
  },
} = response;

console.log(name, age);   // Curtis 30

Pull values from a deeply nested place in one line. No intermediate variables (data, user) — only the leaf values you need.

But going too deep hurts readability. When nesting goes two-plus levels, it’s usually clearer to split into two or three steps or use optional chaining (#5).

Array destructuring — patterns #

basics review + variations
const arr = [1, 2, 3, 4, 5];

const [a, b] = arr;             // a=1, b=2
const [, , c] = arr;             // c=3 (skip first two)
const [first, ...rest] = arr;    // first=1, rest=[2,3,4,5]
const [x = 10] = [];             // x=10 (default)

Order is meaningful in arrays — destructuring receives by order.

Swap — exchange two variables #

classic swap
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b);   // 2 1

A one-line idiom without a temp variable. The old way was let temp = a; a = b; b = temp;.

Parameter destructuring — most common case #

Destructuring in function parameters is one of modern JavaScript’s most common idioms.

options-object pattern
function createUser({ name, age, email = 'unset' }) {
  console.log(name, age, email);
}

createUser({ name: 'Curtis', age: 30 });
// Curtis 30 unset

The pattern’s strengths:

  • Key names visible at the call site — no need to remember argument order
  • Optional arguments expressed via key + default
  • Adding new arguments preserves compatibility — old calls don’t break

A React component’s props is exactly this pattern.

Allow calling with no object #

safe options pattern
function init({ debug = false, retries = 3 } = {}) {
  console.log(debug, retries);
}

init();                       // false 3 — OK without arguments
init({ debug: true });        // true 3

Adding = {} to the parameter itself starts with an empty object when missing, making destructuring safe. The key pattern when all options are optional.

Take some, gather the rest #

rest gathers the rest
function update(user, { id, ...rest }) {
  console.log(id);     // separately
  console.log(rest);   // remaining object
  Object.assign(user, rest);
}

update(targetUser, { id: 'u1', name: 'Curtis', age: 30 });
// id: u1
// rest: { name: 'Curtis', age: 30 }

Extract id for inspection/log/special handling, and collect the rest as an object to pass via spread/assign. Common in library functions.

Spread variations — objects #

1) No deep merge (shallow copy) #

spread is shallow
const a = { user: { name: 'Curtis', age: 30 } };
const b = { ...a, theme: 'dark' };

b.user.age = 31;
console.log(a.user.age);   // 31  ← a changes too

{...a} creates only first-level properties anew. Inner objects still share the same reference (Basics #2). For deep copy:

deep copy — structuredClone
const a = { user: { name: 'Curtis', age: 30 } };
const b = structuredClone(a);

b.user.age = 31;
console.log(a.user.age);   // 30

structuredClone is a built-in standardized in ES2022. It deep-copies most data structures (objects, arrays, Date, Map, Set, etc.). The old trick JSON.parse(JSON.stringify(...)) couldn’t handle functions/Date/undefined.

2) Conditional properties — && and spread #

conditional property
const includeEmail = true;
const profile = {
  name: 'Curtis',
  age: 30,
  ...(includeEmail && { email: 'me@example.com' }),
};

If includeEmail is true, { email: ... } is spread; if false, an empty object is spread (nothing happens).

A pattern often used to build options objects dynamically.

3) Spread into function arguments #

spread function arguments
const args = [1, 2, 3];
Math.max(...args);   // 3

function logAll(a, b, c) {
  console.log(a, b, c);
}
logAll(...args);     // 1 2 3

Modern replacement for apply. The old way was Math.max.apply(null, args).

Spread vs Rest — same ..., different meaning #

The same ... syntax means different things by position.

PositionNameMeaning
Function call argumentspreadunpack array as arguments
Inside array/object literalspreadunpack elements/properties
Function parameterrestgather arguments into array
In a destructuring patternrestgather the rest
comparison by position
const arr = [1, 2, 3];

Math.max(...arr);            // spread — unpack
const copy = [...arr];        // spread — unpack
function f(...args) {}        // rest — gather
const [head, ...tail] = arr;  // rest — gather

Gather on the left → rest; unpack on the right → spread. The shortest mnemonic.

Common practical patterns #

1) immutable update — change one field, new object #

field update
const user = { id: 'u1', name: 'Curtis', age: 30 };

const updated = { ...user, age: 31 };
// original untouched, only age updated

Essential in React setState-like cases.

2) Array — update one element by index #

update one array element
const items = ['a', 'b', 'c', 'd'];
const i = 2;
const newValue = 'C';

const updated = [
  ...items.slice(0, i),
  newValue,
  ...items.slice(i + 1),
];
// ['a', 'b', 'C', 'd']

// or ES2023's toSpliced
const updated2 = items.toSpliced(i, 1, newValue);

toSpliced is shorter. The trend in modern JavaScript is more immutable methods.

3) Merging keys — combine two objects #

object merge
const defaults = { theme: 'light', lang: 'ko' };
const userPrefs = { theme: 'dark' };

const merged = { ...defaults, ...userPrefs };
// { theme: 'dark', lang: 'ko' }

The later object wins on shared keys (priority). Same effect as Object.assign({}, defaults, userPrefs).

4) JSX/React props pass-through #

pass props through
function Wrapper(props) {
  return <Child {...props} />;
}

Very common in React code. A case where the parent passes received props through to a child.

Pitfall — losing prototype info on object spread #

{...obj} takes only obj’s own enumerable properties. The prototype chain isn’t followed.

prototype lost
class User {
  constructor(name) {
    this.name = name;
  }
  greet() {
    console.log(`hi, ${this.name}`);
  }
}

const u = new User('Curtis');
const copy = { ...u };

u.greet();        // hi, Curtis
copy.greet();     // ✗ TypeError — copy is plain, no greet

Spread-copying a class instance produces a plain object. Methods live on the prototype and disappear. Object.assign has the same limit, and structuredClone doesn’t preserve class instances either (turns into a plain object). For real instance copies, give the class its own clone() method is typical.

Wrap-up #

What we covered:

  • Object destructuring — rename, default, combined, nested
  • Array destructuring — swap idiom, rest for head/tail
  • Parameter destructuring — options-object pattern is most common
  • = {} to allow argument-less calls
  • spread is shallow — deep copy via structuredClone
  • Conditional properties — ...(cond && { ... })
  • spread vs rest — left rest / right spread
  • Immutable update patterns
  • Class instances can’t be copied via spread

In the next post (#5 Optional Chaining and Nullish Coalescing) we cover safely accessing deep objects with ?. and the ?? operator that distinguishes nullish from falsy.

X