Destructuring assignment

Destructuring binds values from iterables and object properties, with precise rules for defaults, nesting, rest elements, and shallow copies.

level intermediate time 14 min at Standard depth
version Node 24
what

Destructuring uses a pattern to take values from an iterable or object properties and bind or assign them to several targets.

trap

A default replaces only undefined; a nested pattern does not automatically protect against null or a missing intermediate object.

fix

Define the input contract, then default every level that may be absent. Build public output from an allowlist instead of treating object rest as redaction.

What it is and why it exists

Destructuring assignment is a family of JavaScript syntax in which a destructuring pattern on the left describes how to take data from a value on the right. Array patterns read in iteration order; object patterns read by property key. Destructuring does not mutate the source array or object, although assignment targets and default expressions inside the pattern can have other side effects.

Without destructuring, reading several values from one structure requires repeated property access or indexing. A pattern puts “which fields this code depends on” into one declaration, assignment, or parameter binding. It works best with stable, shallow shapes such as configuration objects, tuple-like function results, and key-value pairs produced while iterating a Map.

Destructuring appears in three positions. A variable declaration creates bindings, as in const { id } = order; an assignment expression updates existing targets, as in ({ id } = order); and a function parameter binds when a call begins, as in function print({ id }) {}. The forms share most pattern syntax, but a declaration can create only valid binding names, while an assignment can also target something such as target.id.

Array and object patterns look related but follow different read rules. const [first] = value requires the right side to implement the iterable protocol, while const { first } = value looks up a property named first. Array destructuring is not shorthand for reading numeric property 0: custom iterables , strings, Set instances, and generators can all be consumed by an array pattern.

An object pattern does not require a plain object either. Every primitive except null and undefined can participate in object destructuring; for example, a string can supply its length property. Lookup follows ordinary JavaScript rules, so inherited properties and getters can also be read.

A colon in a pattern maps a property key to a local target; it is not a type annotation. const { id: orderId } = order reads id but creates only orderId. Later code does not gain an id variable merely because the object has that property.

The ... inside a destructuring pattern is a rest element or rest property. It collects content that the pattern has not taken into a new array or object. It shares a token with a function rest parameter and with spread syntax in array and object literals, but the context and data direction differ: destructuring collects, while spread expands.

Where destructuring fits

Destructuring is clearest when a consumer needs a few fields from a stable data shape. The pattern stays close to the use site, so a reader need not infer dependencies across repeated property access. During Object.entries() or Map iteration, for (const [key, value] of entries) also states directly that each iterated value is a pair.

A computed property pattern still works when the property name is known only at runtime, but direct bracket access is often shorter. A deeply nested field used once does not necessarily deserve a multi-level pattern either. The goal is to make data dependencies clearer, not to eliminate every dot.

Destructuring does not validate untrusted data or generate runtime type checks. An object having the right keys does not mean their values have the business types you need. Validate data from the network, storage, or user input against an explicit schema before passing it to destructuring code that assumes a stable shape.

Object rest and array rest both allocate a new container. For ordinary business data, choose them for clarity instead of guessing about tiny performance differences. If an allocation sits in a measured hot path, benchmark alternatives with representative inputs.

Array destructuring is a good fit for a small tuple when position itself has stable meaning. If an interface may gain optional fields, a named object is usually easier to evolve because callers do not depend on where a new field is inserted.

Deep destructuring in a parameter position removes the direct reference to the complete input. That can make error reporting, audit logging, or multi-field validation harder. When those needs exist, accept a named parameter first and destructure it in steps inside the function body.

Rename a binding to express its meaning in the local scope, not merely to shorten it. When two sources both have an id, names such as orderId and customerId prevent collisions and tell a reviewer where each value came from.

How it works

JavaScript evaluates the right-hand expression once, then processes pattern targets from left to right. One evaluation does not mean one read: a getter can still run twice if the same property appears twice in a pattern. A computed property name is evaluated at its position too, so the key expression itself can have observable side effects.

An array pattern obtains an iterator from the right-hand value and requests the next item for every element position. An elision merely omits a binding; it does not omit the iterator call. When the pattern ends before the iterator does, the runtime performs iterator closing, so a custom iterator’s return() can run.

An array rest element must appear at the end of its pattern. It keeps consuming the iterator and puts the remaining values into a new array. Because this happens eagerly, rest over an infinite iterator never finishes, and rest over a large generator consumes the whole remainder at once.

An object pattern performs ordinary lookup by property key. The shorthand { id } reads key id and binds it to the name id, while { id: orderId } binds the same property value to orderId. { [key]: value } evaluates key first and uses the result as the property key.

An object rest property creates a new ordinary object. It copies the remaining own enumerable string and Symbol keys, not inherited or non-enumerable properties. Copying reads the source value, so getters run; the target receives a data property rather than the original accessor descriptor.

That result is a shallow copy . The outer container is new, but nested objects retain their identity. Mutating a nested object through the rest result can therefore change what you observe through the source too.

When a pattern says target = initializer, JavaScript evaluates initializer only if the extracted value is strictly undefined. A missing property, an out-of-range array position, and an array hole normally produce undefined, so each can trigger a default. null, false, 0, and an empty string do not.

Nested defaults apply one layer at a time. In const { shipping: { city = 'pickup' } = {} } = order, = {} protects the case where shipping is undefined, while city = 'pickup' protects an undefined city. If shipping is explicitly null, the inner object pattern still throws TypeError.

The process is:

  1. Evaluate the right-hand expression and check the minimum requirement of the current pattern.
  2. Read iterator values or property values in pattern order.
  3. Evaluate a default expression only when the read result is undefined.
  4. Write the result to a new binding or existing assignment target, then collect any rest content.

Pattern syntax at a glance

This table maps common forms to their actual targets. Bracket patterns depend on position and iteration order; brace patterns depend on property keys.

PurposePatternResult
First array itemconst [first] = valuesCreates first
Skip one itemconst [, second] = valuesConsumes two items and binds only the second
Remaining array itemsconst [head, ...tail] = valuestail is a new array
Object shorthandconst { id } = valueCreates a same-named binding from key id
Object renameconst { id: orderId } = valueCreates only orderId
Computed keyconst { [key]: picked } = valueEvaluates key first
Remaining object propertiesconst { id, ...rest } = valuerest is a new shallow object
Defaultconst { id = fallback } = valueEvaluates fallback only for undefined

Patterns can be combined freely, but combinations do not always improve readability. A pattern with several levels of defaults, renames, computed keys, and rest properties usually deserves a few named steps. Staged code can also report a different error at each boundary.

Bindings and assignment targets

A declaration pattern follows lexical binding rules. A name created by const cannot be reassigned, a name created by let has a temporal dead zone, and the same scope cannot redeclare a binding. Destructuring does not bypass these rules; it creates several names in one declaration.

An assignment pattern does not create bindings. Its array or object targets must already be assignable, so they can be existing variables, object properties, or suitable index expressions. The assignment expression itself evaluates to the right-hand value, which can occasionally help expression composition, though a standalone statement is usually clearer.

A parameter pattern runs when the function is called. It can combine a parameter default with defaults inside the pattern, and those defaults protect different levels. The parameter default chooses the value to destructure when the argument is undefined; inner defaults handle undefined members read from that value.

Getters, default expressions, and assignments completed before a pattern fails are not rolled back. If a later property getter throws, an earlier target may already have changed. Avoid packing side-effectful assignment targets into a complex pattern, and do not treat destructuring as an atomic transaction.

Examples

These three examples add renaming and nesting, parameter defaults, then assignment to existing variables and object rest. Every output below was produced by Node 24.14.0.

Reading a stable order shape

The first example combines object keys, array positions, renaming, a nested pattern, a default, and array rest. The pattern states directly that later logic needs the order ID, customer name, first line, and remaining lines.

basics.js
const order = {
  id: 'A-17',
  customer: { name: 'Lin' },
  lines: [
    { sku: 'KB-1', quantity: 2 },
    { sku: 'MS-2', quantity: 1 },
  ],
};

const {
  id: orderId,
  customer: { name: customerName },
  lines: [firstLine, ...remainingLines],
  currency = 'EUR',
} = order;

console.log(orderId, customerName, currency);
console.log(firstLine.sku, firstLine.quantity);
console.log(remainingLines.map(({ sku }) => sku).join(','));
A-17 Lin EUR
KB-1 2
MS-2

The object pattern reads by key, so property order in order does not affect the result. The array pattern handles lines positionally: its first item goes to firstLine, and the rest form a new remainingLines array. The source has no currency, so lookup produces undefined and activates the EUR default.

This nested shape is an explicit input contract. If customer or lines may be absent, do not pretend the pattern is inherently safe. Add a default for the relevant level or validate before destructuring; the right choice depends on whether missing data is normal or invalid.

Normalizing input with a parameter pattern

Parameter destructuring can normalize fields that are allowed to be absent as a function starts. The outer = {} handles an omitted or undefined argument, and the inner defaults separately handle missing shipping, totals, and their members.

defaults.js
function normalizeOrder({
  id = 'untracked',
  shipping: { city = 'pickup' } = {},
  totals: [subtotal = 0, tax = 0] = [],
  note = 'none',
} = {}) {
  return { id, city, total: subtotal + tax, note };
}

const complete = normalizeOrder({
  id: 'A-17',
  shipping: {},
  totals: [80, 16],
  note: null,
});

console.log(JSON.stringify(complete));
console.log(JSON.stringify(normalizeOrder({ id: 'B-04' })));
console.log(JSON.stringify(normalizeOrder()));
{"id":"A-17","city":"pickup","total":96,"note":null}
{"id":"B-04","city":"pickup","total":0,"note":"none"}
{"id":"untracked","city":"pickup","total":0,"note":"none"}

The first line preserves note: null because a destructuring default does not handle null. The second line uses object, array, and leaf defaults. The third also uses the default for the whole parameter, so omitting the argument entirely does not throw.

This signature suits an internal configuration in which absent fields have defined replacements. Validate external API responses for type and required fields first. Quietly turning damaged input into business values with defaults lets the failure surface farther from its cause.

Updating bindings and observing a shallow copy

When assigning into existing variables, an object pattern at the start of a statement needs parentheses; otherwise the parser treats { as the start of a block. This example also selects a property with a computed key and shows that object rest copies only one level.

assignment-and-rest.js
const account = {
  id: 'U-3',
  name: 'Mira',
  role: 'admin',
  passwordHash: 'not-for-output',
  profile: { theme: 'dark' },
};

let accountId;
let displayName;
({ id: accountId, name: displayName } = account);

const requestedKey = 'role';
const { [requestedKey]: selectedRole } = account;
const { passwordHash: removed, ...withoutPassword } = account;
withoutPassword.profile.theme = 'light';

console.log(accountId, displayName, selectedRole);
console.log(Object.keys(withoutPassword).join(','));
console.log(account.profile.theme, removed.length);
U-3 Mira admin
id,name,role,profile
light 14

passwordHash is absent from withoutPassword, but this form excludes only one currently known key. If the object later gains refreshToken, the rest object automatically includes it. This pattern is useful for ordinary transformation, not for creating a stable security boundary.

The last line proves the copy is shallow. account.profile and withoutPassword.profile refer to the same object, so mutating theme through the latter makes the former observe light too. When nested state must be isolated, copy explicitly for that data shape or use an appropriate structured-cloning strategy.

Pitfalls

A whole-parameter default does not protect null

Fix: decide whether null is a valid “no value.” If it is, destructure input ?? {} inside the body; if it is not, reject it at the boundary with a clear error. Do not scatter || {} through code without a contract because it also replaces 0, false, and an empty string.

Defaults do not replace every falsy value

Fix: if both null and undefined mean absent, destructure first and then use count ?? 10. Validate explicitly when the value also has a type or range requirement. count || 10 is not a replacement for input rules because it incorrectly overwrites a valid 0.

A leaf default does not protect an intermediate level

Fix: for an intermediate level that may normally be absent, write profile: { name = 'guest' } = {}, and remember that it still rejects profile: null. Validate untrusted external shapes outside the pattern. Beyond two nested levels, staged reads usually express each level’s error policy more clearly.

A colon does not create two variables

Fix: read an object pattern in review as “property key maps to target.” When assigning an existing variable, wrap the full expression as ({ name: displayName } = account). If you need both a parent object and one field, bind both, as in { profile, profile: { name } }, and check whether a repeated getter is acceptable.

Rest properties are not secure redaction

Fix: construct logs, API responses, and data crossing a trust boundary from an allowlist, such as explicitly destructuring id and displayName into a new object. Decide how to copy, freeze, or serialize each nested field. Object rest is convenient when unknown data should be retained; it cannot guarantee that unknown data stays inside a boundary.

Array elisions still advance the iterator

Fix: count next() calls by pattern position during review, not by variable count. If every consumption is expensive or needs confirmation, named iteration steps are clearer. Never use a rest element with an infinite iterator because it attempts to collect every subsequent value.

Deep Evaluation and observable operations

Evaluation and observable operations

Destructuring is not merely a textual abbreviation. An array pattern drives an iterator, an object pattern performs property reads, and default expressions, getters, Proxy traps, and an iterator’s return() can all make evaluation order observable. That boundary matters when reviewing custom collections and hidden side effects in generated code.

The array pattern below contains an elision. It has no target name but still calls next(), so the third binding receives value 3. The pattern ends after that item while the custom iterator has not reported done: true, so the runtime calls return() to close it.

The object half records operations with a Proxy. It reads a and b in pattern order, then evaluates the default only after b produces undefined. Object rest enumerates keys next; excluded a and b are not copied, while c needs an enumerability check and a value read.

Early completion and exceptions

Binding a finite prefix with an array pattern does not mean the iterator naturally reaches done: true. Once the runtime knows the pattern needs no more values, it attempts to close an iterator that has not finished. A generator can run finally cleanup at that point, while a custom iterator can release resources in return().

If next(), an assignment target, or a default expression throws, iterator-closing rules may still run. Closing can fail too, so which error surfaces follows the specification’s completion-record rules. Application code should not depend on accidental replacement among multiple errors; make resource cleanup simple and test it separately.

An object pattern has no matching iterator-close step because it performs property access. A getter or Proxy trap can still throw at any point. Earlier bindings or assignments remain in place, which is why a complex destructuring operation cannot provide all-or-nothing semantics.

Ordinary arrays and plain data objects rarely expose these hooks, but library boundaries can. If an API accepts any iterable or proxy object, document consumption count, early closing, and exception behavior. Callers can then judge whether destructuring matches the resource lifetime.

operations.js
const iteratorTrace = [];
const numbers = {
  [Symbol.iterator]() {
    let current = 1;
    return {
      next() {
        iteratorTrace.push(`next:${current}`);
        return { value: current++, done: false };
      },
      return() {
        iteratorTrace.push('return');
        return { done: true };
      },
    };
  },
};

const [first, , third] = numbers;
console.log(first, third, iteratorTrace.join(' | '));

const propertyTrace = [];
const source = new Proxy({ a: 1, b: undefined, c: 3 }, {
  get(target, key, receiver) {
    propertyTrace.push(`get:${String(key)}`);
    return Reflect.get(target, key, receiver);
  },
  ownKeys(target) {
    propertyTrace.push('ownKeys');
    return Reflect.ownKeys(target);
  },
  getOwnPropertyDescriptor(target, key) {
    propertyTrace.push(`descriptor:${String(key)}`);
    return Reflect.getOwnPropertyDescriptor(target, key);
  },
});

const fallback = () => (propertyTrace.push('default:b'), 2);
const { a, b = fallback(), ...rest } = source;
console.log(a, b, rest.c);
console.log(propertyTrace.join(' | '));
1 3 next:1 | next:2 | next:3 | return
1 2 3
get:a | get:b | default:b | ownKeys | descriptor:c | get:c

The first line also shows that array destructuring handles an iteration sequence, not a fixed set of source-object indexes. If an iterator’s next() reads a network stream, file, or shared queue, an elision still consumes data. During review, treat every comma as a potential iteration advance.

The third line shows that the default expression is lazy. If b were 0 or null, default:b would not appear. A default can also refer to a binding created earlier in the pattern, but referring to a later uninitialized binding triggers a temporal-dead-zone error; depending on that ordering usually hurts readability.

Object-rest copy boundaries

Rest exclusion uses property keys, not final variable names. const { id: orderId, ...rest } = value excludes the source key id. A computed property excludes its evaluated key, including a Symbol key when the computation produces a Symbol.

Only own enumerable properties are copied. A getter on the prototype can be read by an explicit property pattern but does not flow into the rest object automatically. Non-enumerable properties are skipped too, so the rest object cannot preserve all source behavior or metadata.

Copying reads a getter’s current result and creates an ordinary data property on the new object. Reading the rest object later does not run the source getter again. That change may provide the snapshot semantics you want, or it may discard dynamic behavior; the interface contract decides.

Property values are not recursively copied. Arrays, objects, Map and Set instances, and class instances can remain shared between the source and rest result. For actual isolation, define which values are cloneable and choose structuredClone(), domain-specific copy logic, or an immutable data structure; no universal deep-copy operation fits every JavaScript value.

Object-rest key order follows the defined order of an object’s own keys and should not be treated as a security filter. A Proxy can also run arbitrary logic for ownKeys, descriptor lookup, or property getters. Destructuring does not isolate an untrusted object; it simply applies the language’s ordinary object and iteration protocols.

This observability is normally not a performance concern and is no reason to avoid destructuring for plain arrays or data objects. It is a semantic concern: a source with getters, a Proxy, or a custom iterator executes protocol-defined operations. Tests should check call order and exception cleanup instead of assuming that the syntax merely copies a few values.

Further reading

checkpoint

4 questions · 2 predict-the-output · 1 spot-the-bug

Copy as Markdown Interview bank Edit on GitHub Report an error Was this clear?