Computed keys, nested defaults, rest renaming and the undefined versus null pitfall
Destructuring looks simple at first glance, but beyond const { a, b } = obj there are patterns that surprise even experienced developers. This article covers nested defaults, computed property keys, rest combined with renaming, and the classic pitfall between undefined and null.
Table of Contents
- 1. Destructuring beyond the basics
- 2. Nested default values across multiple levels
- 3. Computed property keys while destructuring
- 4. Rest elements combined with renaming
- 5. Destructuring in function parameters with defaults
- 6. Array destructuring with the skip pattern and swap
- 7. The pitfall: defaults only apply to undefined, not null
- 8. Destructuring custom iterable objects
- 9. Readability, performance and recommendation
- 10. Summary
- 11. FAQ
1. Destructuring beyond the basics
The basic form of destructuring, const { a, b } = obj or const [x, y] = arr, is by now everyday tooling. Less well known are the patterns that emerge once defaults, dynamic keys, and rest elements are combined. It is exactly in that combination that most misunderstandings and bugs arise, because individual rules influence each other.
This article deliberately focuses on these advanced combinations rather than the basic syntax. Every pattern is shown with a concrete code example, making clear when reaching for an advanced pattern is actually worthwhile and when a simpler form is enough.
2. Nested default values across multiple levels
Default values can be assigned not just at the top level but simultaneously at every nesting level. This allows a complex, partially incomplete configuration object to be destructured in a single expression with sensible fallbacks, without having to manually check every intermediate level for existence first.
Importantly, a default on an outer level only kicks in if that exact level is undefined. If the outer level is present but a deeper property is missing, a separate default must be defined at that deeper level. Every level needs its own fallback, defaults do not automatically cascade downward.
function renderCard({
title,
layout: { columns = 2, spacing = "md" } = {},
} = {}) {
return `${title} (${columns} columns, spacing ${spacing})`;
}
console.log(renderCard({ title: "Dashboard" }));
// -> Dashboard (2 columns, spacing md), layout missing entirely
console.log(renderCard({ title: "Report", layout: { columns: 3 } }));
// -> Report (3 columns, spacing md), spacing missing at deeper level
3. Computed property keys while destructuring
Just like with object literals, square brackets can be used while destructuring to compute a property name dynamically from a variable or expression. This is especially useful when the key to read is only known at runtime, for example from a function parameter or an iteration.
A common use case is reading a value from a configuration object based on a variable field name, combined with renaming so the local variable still gets a fixed, descriptive name instead of adopting the dynamic key as its identifier.
function readSetting(settings, key) {
const { [key]: value = "n/a" } = settings;
return value;
}
const settings = { theme: "dark", locale: "en-US" };
console.log(readSetting(settings, "theme")); // -> dark
console.log(readSetting(settings, "unknown")); // -> n/a
// computed key combined with renaming
const field = "email";
const { [field]: userEmail } = { email: "a@example.com" };
console.log(userEmail); // -> a@example.com
4. Rest elements combined with renaming
Object rest collects all properties that were not explicitly destructured into a new object, while array rest collects all remaining elements into a new array. Both combine cleanly with renaming of the values destructured before them, which is especially useful in function parameters for options objects.
In function parameters this combination allows pulling out individual, renamed options explicitly while all remaining options land unchanged in the rest object and can, for example, be passed through to another function without listing every property individually.
function createButton({ label: text, onClick: handler, ...rest }) {
console.log(`Button: ${text}`);
return { text, handler, extraProps: rest };
}
const result = createButton({
label: "Save",
onClick: () => {},
disabled: false,
variant: "primary",
});
console.log(result.extraProps); // -> { disabled: false, variant: "primary" }
const [head, ...tail] = [1, 2, 3, 4];
console.log(head, tail); // -> 1 [2, 3, 4]
5. Destructuring in function parameters with defaults
With function parameters there are two different levels of defaults that are easily confused. A default for the entire parameter object, written as = {} after the closing curly brace, only applies when no argument at all is passed at the call site. Defaults for individual properties inside the braces apply independently of whether the object itself was passed.
In practice, combining both levels is the standard approach for options objects: the outer default makes the whole parameter optional, while the inner defaults supply sensible values for individual, likewise optional properties. If either of these two levels is missing, the caller must either always pass an object or always set every property explicitly.
function fetchUsers({ page = 1, pageSize = 20, sortBy = "name" } = {}) {
return `GET /users?page=${page}&size=${pageSize}&sort=${sortBy}`;
}
console.log(fetchUsers());
// -> GET /users?page=1&size=20&sort=name, no argument passed at all
console.log(fetchUsers({ page: 2 }));
// -> GET /users?page=2&size=20&sort=name, only one property set
6. Array destructuring with the skip pattern and swap
In array destructuring, individual elements can be skipped by simply leaving an empty comma in their place. This is especially useful when only certain positions of an array matter, for example the first and third element from the result of a regular expression match.
Another popular pattern is swapping variables without a temporary helper variable, made possible by array destructuring on both sides of the assignment. Destructuring works with any iterable object here, not just real arrays, including map entries or the return value of a generator.
const [, second, , fourth] = ["a", "b", "c", "d"];
console.log(second, fourth); // -> b d
let x = 1;
let y = 2;
[x, y] = [y, x];
console.log(x, y); // -> 2 1
const map = new Map([["id", 1], ["name", "Alice"]]);
for (const [key, value] of map) {
console.log(key, value);
}
7. The pitfall: defaults only apply to undefined, not null
Probably the most common bug with destructuring defaults happens because a default value only kicks in when the property in question is strictly undefined. If an API instead explicitly returns null, for example because an optional field was not set in the database, the destructured value stays null and the default is ignored.
The usual fix is explicit normalization before destructuring, for example applying the nullish coalescing operator to the whole object or to individual fields before destructuring happens. Alternatively, the destructured value can be renormalized directly afterward using ??.
function greet({ name = "Guest" } = {}) {
return `Hello, ${name}`;
}
console.log(greet({ name: undefined })); // -> Hello, Guest
console.log(greet({ name: null })); // -> Hello, null (default does NOT apply)
// fix: normalize afterward
function greetSafe({ name } = {}) {
const safeName = name ?? "Guest";
return `Hello, ${safeName}`;
}
console.log(greetSafe({ name: null })); // -> Hello, Guest
8. Destructuring custom iterable objects
Array destructuring is not limited to real arrays, it works with any object that implements the iterable protocol, meaning it has a Symbol.iterator. This lets a custom class be designed so it can be destructured just as naturally as a native array.
This is especially useful for custom data structures like ranges, coordinates, or result objects returned by a library. Consuming code can then write const [start, end] = range without knowing that range is not actually an array internally, but merely implements the iterable protocol.
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
*[Symbol.iterator]() {
yield this.start;
yield this.end;
}
}
const range = new Range(10, 20);
const [start, end] = range;
console.log(start, end); // -> 10 20
9. Readability, performance and recommendation
Advanced destructuring patterns are powerful, but excessive nesting can hurt readability because it is no longer clear at a glance what structure is expected. As a rule of thumb, deliberately limit nesting depth and the number of defaults in a single expression, and split complex cases into several steps instead.
Performance is not a relevant factor for any of the shown patterns in practice, modern engines optimize destructuring efficiently. What matters instead is maintainability: a pattern should always be chosen so the intent of the code stays immediately recognizable for subsequent readers, not as compact as technically possible.
| Pattern | Syntax Example | Key Pitfall | Typical Use |
|---|---|---|---|
| Nested Defaults | { a: { b = 1 } = {} } | Defaults do not cascade downward | Configuration objects with fallbacks |
| Computed Keys | { [key]: value } | Key must exist at runtime | Dynamic access to object fields |
| Rest Combined with Renaming | { a: x, ...rest } | Rest always creates a new object | Options objects in functions |
| Skip and Swap | [, b, , d] | Empty commas are easy to overlook | Selective array access, swapping values |
| Iterable Destructuring | const [a, b] = obj | Requires Symbol.iterator | Custom data structures like ranges |
Mironsoft
Modern browser APIs, performance, and maintainable JavaScript
JavaScript that holds up in the real browser, not just in the tutorial?
We review existing frontend code for outdated patterns, unnecessary dependencies, and performance traps, then replace them with modern, native browser APIs that mean less bundle weight and less maintenance burden.
Code Review
Systematically finding outdated patterns, unnecessary dependencies, and memory leaks.
Performance Optimization
Improving bundle size, load time, and runtime performance with modern APIs.
Modernization
Deliberately introducing native browser APIs instead of heavy libraries.
10. Summary
Destructuring Patterns: The Essentials at a Glance
Defaults
Apply only for undefined, every level needs its own fallback
Computed Keys
Square brackets allow dynamic property names while destructuring
Rest
Collects remaining properties or elements, combinable with renaming
Iterables
Array destructuring works with any object implementing Symbol.iterator