09 — Destructuring Internals: Pattern Matching, Defaults & the null Trap
Array Destructuring: Positional Binding
javascript
// ── Basic positional binding ──
const [a, b, c] = [1, 2, 3];
console.log(a, b, c); // 1 2 3
// ── Skip elements (holes in the pattern) ──
const [first, , third] = [1, 2, 3];
console.log(first, third); // 1 3 (second element skipped)
// ── Rest pattern (collect remaining elements) ──
const [head, ...rest] = [1, 2, 3, 4];
console.log(head); // 1
console.log(rest); // [2, 3, 4] (real Array)
// ── Default values (used when the element is undefined) ──
const [x = 10, y = 20] = [5];
console.log(x, y); // 5 20 (x got 5, y is undefined → default 20)
// ── Swapping variables (no temp needed) ──
let left = 1, right = 2;
[left, right] = [right, left]; // left=2, right=1
// ── Swapping without a temp (engine-level XOR swap is not used — JS creates a temp array) ──
// [a, b] = [b, a] creates a temporary array [b, a], then destructures it.
// Not as efficient as a true XOR swap, but clear and idiomatic.
// ── Destructuring from a function return ──
function getCoords() { return [10, 20]; }
const [x2, y2] = getCoords(); // x2=10, y2=20
// ── Destructuring in a for...of loop ──
const entries = [["a", 1], ["b", 2], ["c", 3]];
for (const [key, value] of entries) {
console.log(key, value); // "a" 1, "b" 2, "c" 3
}
for (const [index, value] of ["x", "y", "z"].entries()) {
console.log(index, value); // 0 "x", 1 "y", 2 "z"
}
Object Destructuring: Property Binding by Name
javascript
// ── Basic property binding (match by property name) ──
const { name, age } = { name: "Alice", age: 30 };
console.log(name, age); // "Alice" 30
// ── Rename: bind to a different variable name ──
const { name: userName, age: userAge } = { name: "Alice", age: 30 };
console.log(userName, userAge); // "Alice" 30
// Syntax: { sourceProperty: targetVariable }
// ── Default values (used when the property is undefined) ──
const { port = 3000, host = "localhost" } = { port: 8080 };
console.log(port, host); // 8080 "localhost" (host was undefined → default)
// ── Rename + default ──
const { port: p = 3000, host: h = "localhost" } = { port: 8080 };
console.log(p, h); // 8080 "localhost"
// ── Nested destructuring ──
const user = {
name: "Alice",
profile: {
email: "alice@example.com",
address: { city: "NYC", zip: "10001" },
},
};
const { profile: { email, address: { city, zip } } } = user;
console.log(email, city, zip); // "alice@example.com" "NYC" "10001"
// Note: `profile` and `address` are NOT bound — only the leaf properties (email, city, zip).
// ── Rest pattern in object destructuring (collect remaining properties) ──
const { name: n, ...rest } = { name: "Alice", age: 30, role: "admin" };
console.log(n); // "Alice"
console.log(rest); // { age: 30, role: "admin" }
// ── Computed property names in destructuring ──
const key = "name";
const { [key]: value } = { name: "Alice" };
console.log(value); // "Alice"
Anti-Pattern: Destructuring null Throws
javascript
// ❌ NAIVE — destructuring null or undefined throws a TypeError
function getUserInfo(user) {
const { name, email } = user; // ✗ if user is null → TypeError: Cannot destructure property 'name' of null
return { name, email };
}
getUserInfo(null); // TypeError!
// ✅ CORRECT — provide a default empty object
function getUserInfoSafe(user) {
const { name, email } = user ?? {}; // if user is null → {} → name/email are undefined
return { name, email };
}
getUserInfoSafe(null); // { name: undefined, email: undefined } (no error)
// ✅ ALSO GOOD — default parameter
function getUserInfoSafe2({ name, email } = {}) {
return { name, email };
}
getUserInfoSafe2(null); // ✗ still throws! The default only applies when NO arg is passed.
// Default param only triggers on `undefined` (no argument), not on `null`!
getUserInfoSafe2(undefined); // ✓ uses default {} (no arg → undefined → default)
getUserInfoSafe2(); // ✓ uses default {} (no arg → default)
getUserInfoSafe2(null); // ✗ TypeError (null is passed, not undefined — no default!)
// ── The null vs undefined distinction in defaults ──
// Default values trigger ONLY when the value is `undefined`:
// - undefined → uses default
// - null → does NOT use default (null is a real value, kept as-is)
const { x = "default" } = { x: undefined }; // x = "default" (undefined triggers default)
const { y = "default" } = { y: null }; // y = null (null does NOT trigger default!)
const { z = "default" } = {}; // z = "default" (missing → undefined → default)
Production Pattern: Safe Deep Destructuring
javascript
// ── Safe deep destructuring with ?? {} at each level ──
function parseApiResponse(response) {
// Each level gets ?? {} so null/undefined at any depth doesn't throw
const {
data: {
user: {
name = "Anonymous",
profile: {
email = "no-email@example.com",
} = {},
} = {},
} = {},
} = response ?? {};
return { name, email };
}
// Works with any input:
parseApiResponse(null); // { name: "Anonymous", email: "no-email@example.com" }
parseApiResponse({}); // { name: "Anonymous", email: "no-email@example.com" }
parseApiResponse({ data: null }); // { name: "Anonymous", email: "no-email@example.com" }
parseApiResponse({ data: { user: null } }); // { name: "Anonymous", email: "no-email@example.com" }
parseApiResponse({ data: { user: { name: "Alice" } } }); // { name: "Alice", email: "no-email@example.com" }
// ── Alternative: optional chaining + nullish coalescing (no destructuring) ──
function parseApi2(response) {
return {
name: response?.data?.user?.name ?? "Anonymous",
email: response?.data?.user?.profile?.email ?? "no-email@example.com",
};
}
// Optional chaining is simpler for read-only access, but destructuring is better
// when you need to extract many properties from the same object.
Destructuring in Function Parameters
javascript
// ── Function parameter destructuring with defaults ──
function configure({
host = "localhost",
port = 3000,
ssl = false,
timeout = 30000,
} = {}) {
// The `= {}` default is for the WHOLE parameter (when no arg or undefined is passed)
console.log(`${ssl ? "https" : "http"}://${host}:${port} (timeout: ${timeout}ms)`);
}
configure(); // http://localhost:3000 (timeout: 30000ms)
configure({ port: 8080 }); // http://localhost:8080 (timeout: 30000ms)
configure({ ssl: true, host: "api.example.com" }); // https://api.example.com:3000
configure(undefined); // http://localhost:3000 (undefined → default {})
// configure(null); // ✗ TypeError: Cannot destructure property 'host' of null
// (null does NOT trigger the `= {}` default — only undefined does)
// ── Destructuring with renaming in parameters ──
function process({ input: source, output: destination, format = "json" }) {
console.log(`${source} → ${destination} (${format})`);
}
process({ input: "file.txt", output: "file.json" }); // file.txt → file.json (json)
// ── Mixed positional and destructured parameters ──
function fetchData(url, { method = "GET", headers = {}, body = null } = {}) {
// First param is positional (url), second is destructured (options object)
console.log(`${method} ${url}`, { headers, body });
}
fetchData("/api/users", { method: "POST", body: "data" });
💡 Tips & Tricks
javascript
// ── Destructuring for swapping without a temp ──
let a = 1, b = 2;
[a, b] = [b, a]; // a=2, b=1
// ── Destructuring for extracting values from regex matches ──
const [fullMatch, year, month, day] = "2024-01-15".match(/^(\d{4})-(\d{2})-(\d{2})$/);
console.log(year, month, day); // "2024" "01" "15"
// ── Destructuring Map entries ──
const config = new Map([["host", "localhost"], ["port", "3000"]]);
for (const [key, value] of config) {
console.log(key, value); // "host" "localhost", "port" "3000"
}
// ── Destructuring to pick properties (omit rest) ──
const user = { id: 1, name: "Alice", email: "a@b.com", password: "secret" };
const { password, ...safeUser } = user; // strip password from the rest
console.log(safeUser); // { id: 1, name: "Alice", email: "a@b.com" }
// ── Destructuring for multiple return values (tuple-like) ──
function divmod(a, b) {
return [Math.trunc(a / b), a % b]; // return [quotient, remainder]
}
const [quotient, remainder] = divmod(17, 5); // 3, 2
// ── Deep default with nullish coalescing ──
const { deep = { nested: "default" } } = obj ?? {};
// If obj is null → {} → deep is undefined → default { nested: "default" }
⚠️ Edge Cases & Gotchas
javascript
// ── Destructuring null/undefined throws (not like access which returns undefined) ──
const { x } = null; // TypeError: Cannot destructure property 'x' of null
const { y } = undefined; // TypeError: Cannot destructure property 'y' of undefined
// Fix: const { x } = obj ?? {};
// ── Default values trigger on undefined, NOT on null ──
const { a = "default" } = { a: undefined }; // a = "default" (undefined triggers)
const { b = "default" } = { b: null }; // b = null (null does NOT trigger default)
const { c = "default" } = {}; // c = "default" (missing → undefined → default)
// ── Rest must be LAST in destructuring ──
const [first, ...rest, last] = [1, 2, 3]; // ✗ SyntaxError: Rest element must be last
const { a, ...rest, b } = obj; // ✗ SyntaxError: Rest element must be last
// ✅ Correct: const [first, ...rest] = [1, 2, 3]; rest is [2, 3] (includes last)
// ── Array destructuring is positional (order matters) ──
const [a, b] = [1, 2]; // a=1, b=2 (positional — index-based)
const { a: x, b: y } = { a: 1, b: 2 }; // x=1, y=2 (named — order doesn't matter)
// Array patterns bind by position; object patterns bind by name.
// ── Destructuring with computed keys requires the property to exist ──
const key = "dynamic";
const { [key]: val } = { dynamic: 42 };
console.log(val); // 42
const { [key]: val2 = "default" } = {};
console.log(val2); // "default" (property missing → undefined → default)
// ── Function param destructuring with null ──
function f({ a = 1 } = {}) { console.log(a); }
f(); // 1 (no arg → undefined → default {} → a defaults to 1)
f(undefined); // 1 (undefined → default {} → a defaults to 1)
f(null); // ✗ TypeError (null does NOT trigger the `= {}` default!)
f({}); // 1 (empty object → a is undefined → defaults to 1)
🧠 Quick Quiz
What does this output?
javascript
const obj = { a: 1, b: null, c: undefined };
const { a = "d", b = "d", c = "d", d = "d" } = obj;
console.log(a, b, c, d);
Answer
1 "d" "d" "d"
a=1— property exists with value1(not undefined, so no default)b="d"— property isnull. Wait...nulldoes NOT trigger defaults! Sobshould benull...
Let me re-check: null does NOT trigger the default. The default only triggers for undefined.
So:
a=1— exists, value is1, not undefined → keeps1b=null— exists, value isnull, NOT undefined → keepsnull(no default!)c="d"— exists, value isundefined→ triggers default"d"d="d"— doesn't exist (missing) → equivalent to undefined → triggers default"d"
Output: 1 null "d" "d"
The lesson: default values in destructuring trigger only when the value is undefined (explicitly or because the property is missing). null is a real value and does NOT trigger defaults. This is the #1 destructuring gotcha.