JAVASCRIPT / DESTRUCTURING, ITERABLES, AND GENERATORS
Object destructuring with defaults and renames
Read object properties under new variable names, supply fallbacks that fire only on undefined, and combine both on nested and parameter patterns.
What you will learn
- Rename a property with key: newName, keeping the source key left of the colon
- Add = fallback after the binding name; it fires only when the read yields undefined
- Guard nested patterns with = {} so a missing parent object does not throw
- Default a destructured parameter to {} so the function can be called with no argument
Understanding Object destructuring with defaults and renames
An object pattern is not a mirror of the object; it is a list of instructions shaped like key: target = default. Whatever sits left of the colon is treated as a property name and used for the lookup, and whatever sits right of it is the binding that receives the value. That split is the whole reason renaming exists: property keys can be strings like 'content-type' that are not legal identifiers, and a key like name may collide with a variable already in scope. When you write no colon at all, const { status } = res is just shorthand for status: status.
A default runs on exactly one condition: the property read produced undefined. The engine never asks whether the key exists, so { tag: undefined } and {} behave identically, and it never asks about truthiness, so 0, '', false, NaN and null are all kept as they are. That makes destructuring defaults stricter than ??, which also falls back on null, and far stricter than ||. If your fallback needs to cover null too, destructure first and apply ?? to the resulting binding yourself.
Bindings are produced left to right, and each default expression is evaluated lazily, only when it is needed, in a scope where the earlier bindings already exist. That is why { first, display: shown = first } works while the reverse order throws a ReferenceError the moment that default has to run. Nesting composes the same way: meta: { retries } performs a second lookup on the result of the first, so if meta is undefined the inner lookup throws before any inner default is considered, which is what = {} on the nested pattern fixes. Note that the nested form binds only retries; meta itself never becomes a variable.
const response = {
status: 200,
'content-type': 'application/json',
body: null,
meta: { retries: 0 }
};
const {
status, // shorthand for status: status
'content-type': contentType, // key on the left, binding name on the right
body = 'empty', // present as null, so the default is skipped
charset = 'utf-8', // key absent, so the default is used
meta: { retries, backoffMs = 250 }
} = response;
console.log(status, contentType);
console.log(body, charset);
console.log(retries, backoffMs);
console.log(typeof meta); // the nested pattern creates no 'meta' bindingIn an object pattern, key: name decides which property is read and under what name, and = value replaces the result only when that read yields undefined.
Worked examples
Only undefined triggers a default
Shows which existing values survive a default and which ones are replaced.
const opts = { retries: 0, label: '', timeout: null, tag: undefined };
const {
retries = 3,
label = 'default',
timeout = 1000,
tag = 'none',
mode = 'fast'
} = opts;
console.log(retries, JSON.stringify(label), timeout, tag, mode);Example explained
Line 1retries stays 0 because 0 is not undefined; the default is not a falsy check like ||.
Line 2label stays the empty string for the same reason, printed through JSON.stringify so it is visible.
Line 3timeout stays null: null is a real value, so the 1000 default never runs, unlike with ??.
Line 4tag falls back even though the key exists, because its value is undefined; mode falls back because the key is absent.
Rename and default in a parameter list
Demonstrates left-to-right defaults that reference earlier bindings, plus a default for the whole parameter.
function makeLabel({ first = 'Anon', last = 'User', display: shown = first + ' ' + last } = {}) {
return shown;
}
console.log(makeLabel({ first: 'Ada', last: 'Lovelace' }));
console.log(makeLabel({ first: 'Ada' }));
console.log(makeLabel({ display: 'A.L.' }));
console.log(makeLabel());Example explained
Line 1display: shown reads the display property but exposes it inside the function as shown.
Line 2shown's default can use first and last because those bindings are created before it, left to right.
Line 3In the third call display is present, so its default expression is never evaluated at all.
Line 4The trailing = {} lets makeLabel() run with no argument; without it that call throws a TypeError.
Guarding a nested pattern
Shows why a missing parent object needs its own default rather than a default on the inner property.
const rows = [
{ id: 1, author: { name: 'Ada' } },
{ id: 2 }
];
for (const { id, author: { name: authorName = 'anonymous' } = {} } of rows) {
console.log(id, authorName);
}Example explained
Line 1author: { ... } reaches one level deeper and binds nothing called author.
Line 2The = {} after the nested pattern runs for row 2, where author is undefined, so an empty object is destructured instead of throwing.
Line 3Only after that step does the inner default = 'anonymous' get a chance to supply a value.
Line 4name: authorName renames the binding so it reads clearly and cannot shadow another name in scope.
Important notes
Destructuring null or undefined throws a TypeError before any default is consulted; defaults cover missing properties, not a missing source object.
Defaults are evaluated in order, so an earlier default cannot reference a later binding: it hits the temporal dead zone and throws a ReferenceError when it runs.
Common mistakes
Expecting a default to behave like ||: with { count: 0, port: null }, const { count = 10, port = 8080 } yields 0 and null, so downstream code receives null where it expected 8080.
Reversing the rename direction and writing { userName: name } when the object has a name key: JavaScript looks up userName, finds nothing, and binds undefined (or the default) to name, which quietly hides the bug.
Writing const { user: { id } } = res without a guard: when user is absent the destructuring throws a TypeError, and a default on id cannot help because the failure happens one level above it.
Try it yourself
Change, predict, then run
Start from const cfg = { host: 'localhost', port: null, 'max-retries': 0 }; and destructure it into host, port defaulting to 8080, maxRetries defaulting to 5, and scheme defaulting to 'https'. Log all four and say for each one whether the default was used and why.
Open the JavaScript workspaceCheck your understanding
Given const opts = { level: 0, mode: null }; and const { level = 5, mode = 'auto', name: title = 'untitled' } = opts; what are level, mode and title?
- 0, null, 'untitled'
- 5, 'auto', 'untitled'
- 0, 'auto', 'untitled'
- 0, null, undefined
Show answer
A default replaces the value only when the property read returns undefined, so level keeps 0 and mode keeps null, while the absent name key leaves title with 'untitled'. Option 3 is tempting if you assume the default works like ??, but ?? also falls back on null and destructuring does not.