JavaScript Destructuring
Destructuring pulls values out of arrays and properties out of objects in a single, declarative expression. It replaces dozens of const x = obj.x; const y = obj.y; lines with one. Once it clicks, it’s the syntax you’ll use a hundred times a day — in function parameters, in imports, in React props, anywhere data flows.
Array Destructuring
const [a, b, c] = [1, 2, 3];
// a = 1, b = 2, c = 3
const [first] = [10, 20, 30];
// first = 10 — extras ignored
const [, , third] = [10, 20, 30];
// third = 30 — skip with commas
const [x = 99] = [];
// x = 99 — default kicks in when value is undefined
const [head, ...tail] = [1, 2, 3, 4];
// head = 1, tail = [2, 3, 4] — rest collects the leftovers
The classic swap — no temp variable needed:
let a = 1, b = 2;
[a, b] = [b, a];
// a = 2, b = 1
Works on anything iterable, not just arrays:
const [r, g, b] = "abc"; // r = "a", g = "b", b = "c"
const [first, second] = new Set([10, 20, 30]);
Object Destructuring
const user = { name: "Ada", age: 37, role: "admin" };
const { name, age } = user;
// name = "Ada", age = 37
// Rename
const { name: userName, role: userRole } = user;
// userName = "Ada", userRole = "admin"
// Default
const { country = "Unknown" } = user;
// country = "Unknown" — `country` was missing
// Rename + default
const { country: c = "n/a" } = user;
// c = "n/a"
// Rest — collect remaining properties
const { name: n, ...rest } = user;
// n = "Ada", rest = { age: 37, role: "admin" }
Order doesn’t matter for objects (it’s by key, not position):
const { age, name } = user; // same result as { name, age }
Nested Destructuring
You can destructure deep into structure in one line:
const response = {
data: {
user: { name: "Ada", address: { city: "Mumbai" } },
},
status: 200,
};
const { data: { user: { name, address: { city } } }, status } = response;
// name = "Ada", city = "Mumbai", status = 200
Two cautions with deep destructuring:
- Readability falls off a cliff beyond two levels.
- Any missing intermediate throws:
const { a: { b } } = {}; // ❌ TypeError — `a` is undefined, can't read .b
A safer form when intermediates might be missing:
const { a: { b } = {} } = {};
// b = undefined — default for the missing `a`
Or just use optional chaining instead:
const b = obj?.a?.b;
Destructuring in Function Parameters
The single biggest use case — “named arguments”:
function createUser({ name, age, role = "user" } = {}) {
return { name, age, role };
}
createUser({ name: "Ada", age: 37 });
// { name: "Ada", age: 37, role: "user" }
createUser(); // works thanks to `= {}` default
The trailing = {} is critical. Without it, calling createUser() with no argument throws because you can’t destructure undefined.
Arrays in parameters work the same way:
function add([a, b]) {
return a + b;
}
add([1, 2]); // 3
Destructuring Return Values
A function returning multiple things via an object:
function parseFullName(name) {
const [first, ...rest] = name.split(" ");
return { first, last: rest.join(" ") };
}
const { first, last } = parseFullName("Ada Augusta Lovelace");
// first = "Ada", last = "Augusta Lovelace"
Or via a tuple-like array:
function minMax(arr) {
return [Math.min(...arr), Math.max(...arr)];
}
const [low, high] = minMax([5, 3, 9, 1]);
// low = 1, high = 9
Iterating an Object With Destructuring
Object.entries + destructured for…of is the readable form:
const scores = { Ada: 95, Lin: 88, Rae: 91 };
for (const [name, score] of Object.entries(scores)) {
console.log(`${name}: ${score}`);
}
// Output:
// Ada: 95
// Lin: 88
// Rae: 91
Same trick in map/filter:
const passed = Object.entries(scores)
.filter(([, score]) => score >= 90)
.map(([name]) => name);
// ["Ada", "Rae"]
Note the [, score] — skipping the unused name with a leading comma.
Destructured Imports
A common destructuring case you may not have recognised:
import { readFile, writeFile } from "node:fs/promises";
const { useState, useEffect } = React;
Mechanically the same as object destructuring.
Rename and Default Together
The most expressive form combines rename + default + nested:
function configure({
host: serverHost = "localhost",
port: serverPort = 8080,
tls: { enabled: tlsEnabled = false, cert } = {},
} = {}) {
console.log({ serverHost, serverPort, tlsEnabled, cert });
}
configure();
// { serverHost: "localhost", serverPort: 8080, tlsEnabled: false, cert: undefined }
configure({ host: "api.example.com", tls: { enabled: true, cert: "abc.pem" } });
// { serverHost: "api.example.com", serverPort: 8080, tlsEnabled: true, cert: "abc.pem" }
Destructuring vs Accessing — When to Use Which
| Situation | Use |
|---|---|
| Pulling 2+ properties out of one object | destructure |
| Single property, used once | direct access (obj.x) |
| Function with 3+ params, especially with defaults | destructure parameters |
Iterating array of [key, value] pairs |
destructure |
| Dynamic property name | bracket access (obj[key]) |
| Possibly-missing intermediates | optional chaining (obj?.a?.b) |
Tip: Don’t over-destructure. If you only need user.name once, write user.name. Destructuring shines when you’re using multiple properties or aliasing them.
A Realistic Example — HTTP Handler
async function handleRequest(req, res) {
// Destructure incoming
const {
method,
url,
headers: { "x-request-id": requestId = crypto.randomUUID() },
} = req;
// Destructure parsed query
const { page = 1, limit = 20, q = "" } = parseQuery(url);
// Pretend this calls a service that returns { items, total }
const { items, total } = await db.search({ q, offset: (page - 1) * limit, limit });
res.json({ requestId, method, page, total, items });
}
Three nested objects flattened into one block of named locals. Compare with the manual form:
// Without destructuring — more typing, more places for typos
const method = req.method;
const url = req.url;
const requestId = (req.headers && req.headers["x-request-id"]) || crypto.randomUUID();
const query = parseQuery(url);
const page = query.page === undefined ? 1 : query.page;
const limit = query.limit === undefined ? 20 : query.limit;
const q = query.q === undefined ? "" : query.q;
// ...
Common Mistakes
// ❌ No default for the destructured object
function fn({ x }) {}
fn(); // TypeError — cannot destructure undefined
// ✅
function fn({ x } = {}) {}
// ❌ Destructuring null
const { x } = null; // TypeError
// ❌ Variable name vs key — easy typo
const { naame } = { name: "Ada" };
// naame = undefined (silent!)
// ❌ Trying to destructure before declaring
{
let { x } = obj;
let x = 1; // SyntaxError — already declared via destructuring
}
// ❌ Re-using a name across assignment in array destructuring
let a, b;
[a, b] = obj; // works
let [a, b] = obj; // ❌ a already declared
// ✅ Use { } syntax once
Best Practices Checklist
- ✅ Destructure function parameters with defaults for cleaner APIs
- ✅ Always default the destructured object:
({...} = {}) - ✅ Use rename to give better local names:
({ id: userId }) - ✅ Combine with optional chaining when intermediates can be missing
- ✅ Use destructuring in
for…of Object.entries(obj)for clean iteration - ❌ Don’t go more than two levels deep — extract intermediate steps
- ❌ Don’t destructure a single property that’s used once
- ❌ Don’t forget the
= {}default — it’s the most common bug
What’s Next?
You can pull data out of structures concisely. The inverse is the spread operator (...), which puts pieces back together. Plus the matching rest parameter you’ve already met. Next chapter covers both in detail.