JavaScript · Modern JavaScript (ES6+)

JavaScript Destructuring, Spread & Rest — The Complete Guide to Objects and Arrays

6 min read Updated 2026-06-18

Practice Destructuring, Spread & Rest interview questions

Three features that reshaped JavaScript

Few ES2015 additions changed daily JavaScript as much as destructuring, spread, and rest. Together they let you pull values out of objects and arrays, copy and merge data structures, and collect variable arguments — all concisely and declaratively. They appear in nearly every modern codebase: React props, function options, immutable state updates, and API response handling. This guide focuses on the object side (array destructuring has its own guide) plus the general rest/spread mechanics that apply everywhere.

Object destructuring basics

Object destructuring extracts properties into variables by name, matching keys to variable names.

const user = { name: 'Ada', age: 36, city: 'London' }
const { name, age } = user
name   // 'Ada'
age    // 36 matched by property name, not position

Unlike array destructuring (by position), object destructuring matches by key — order doesn't matter, only the names.

Renaming and defaults

You can rename a destructured property and provide defaults for missing ones. Defaults apply only when the value is undefined.

const { name: fullName, role = 'guest' } = user
fullName   // 'Ada' (renamed)
role       // 'guest' (default — user has no role)

const { count = 0 } = { count: null }
count      // null default not applied (null is not undefined)

Rename with key: newName and combine with a default: { key: newName = fallback }. This is extremely common when adapting external data to local naming.

Nested destructuring

Patterns can mirror nested structure, reaching deep into objects in one statement.

const config = { server: { host: 'localhost', port: 8080 } }
const { server: { host, port } } = config
host   // 'localhost'
port   // 8080

Be careful: server itself is not bound here — only host and port. And destructuring a missing nested object throws, so guard with defaults: const { server: { port } = {} } = config to avoid crashing when server is absent.

Computed property keys

You can destructure a dynamic key using bracket syntax — useful when the property name is in a variable.

const key = 'age'
const { [key]: value } = user
value   // 36 destructured the property named by `key`

The [key]: part says "the property whose name is the value of key," and value is where it lands.

Destructuring function parameters

A hugely popular pattern: destructure an options object right in the parameter list, with defaults, so callers pass named arguments in any order.

function fetchData({ url, method = 'GET', retries = 3 } = {}) {
  // ...
}
fetchData({ url: '/api', retries: 5 })   // named, order-independent
fetchData()                               // works — the `= {}` default prevents a throw

The trailing = {} is essential: without it, calling with no argument tries to destructure undefined and throws.

The rest pattern in objects

A rest element in destructuring collects the remaining properties into a new object — perfect for "extract a few fields, keep the rest."

const { id, ...rest } = { id: 1, name: 'Ada', age: 36 }
id     // 1
rest   // { name: 'Ada', age: 36 } everything else

This is the idiomatic way to omit a property immutably (here, removing id), since rest is a new object without it.

Object spread

The spread operator in object literals copies an object's enumerable own properties into a new object — the basis of shallow copying and merging.

const original = { a: 1, b: 2 }
const copy = { ...original }              // shallow copy
const merged = { ...original, c: 3 }      // add a property
const updated = { ...original, b: 20 }    // override b

On key conflicts, later sources win — that "override order" powers config/defaults patterns: { ...defaults, ...userOptions } lets user options override defaults.

Spread vs Object.assign

Both spread and Object.assign shallow-merge objects. Spread creates a fresh object; assign mutates its first argument (unless you pass {}).

const merged1 = { ...a, ...b }              // new object preferred
const merged2 = Object.assign({}, a, b)     // same result
const mutated = Object.assign(a, b)         // mutates `a`

Prefer spread for readability and to avoid accidental mutation. Object.assign is still useful when you intentionally want to mutate a target or copy onto an existing object.

The shallow-copy pitfall

The single most important caveat: object spread and Object.assign are shallow. Nested objects are shared by reference, not cloned.

const state = { user: { name: 'Ada' } }
const copy = { ...state }
copy.user.name = 'Grace'
state.user.name   // 'Grace' nested object was shared

To update nested data immutably, spread at each level you change:

const updated = { ...state, user: { ...state.user, name: 'Grace' } }   // new nested object

For arbitrarily deep clones of plain data, use structuredClone(state).

Rest parameters in functions

Beyond destructuring, ... in a function signature creates a rest parameter that gathers extra arguments into a real array — the modern replacement for arguments.

function sum(...nums) {
  return nums.reduce((a, b) => a + b, 0)   // nums is a true array
}
sum(1, 2, 3, 4)   // 10

Same token, opposite jobs: rest collects (in a pattern/signature), spread expands (in a literal/call). Position tells you which: ... on the binding side is rest; on the value side it's spread.

Combining everything

These features compose into expressive one-liners common in real code:

function updateUser(user, { ...changes }) {
  return { ...user, ...changes, updatedAt: Date.now() }   // immutable update
}

const [first, ...others] = items          // array rest
const { theme, ...settings } = preferences // object rest

Key takeaways

  • Object destructuring matches by name; supports renaming (key: alias), defaults (for undefined only), nested patterns, and computed keys.
  • Destructuring parameters with a = {} default is the clean way to accept options objects.
  • The rest pattern ({ a, ...rest }) collects remaining properties — great for omitting a field immutably.
  • Object spread copies/merges with "last wins" ordering; prefer it over Object.assign to avoid mutation.
  • Spread is shallow — spread at each nested level you change, or use structuredClone.
  • Rest parameters gather extra arguments into a real array; rest collects, spread expands.

These three features are the connective tissue of modern JavaScript — fluency with them makes data transformation, immutable updates, and flexible APIs feel effortless.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.