What Is Destructuring?
Destructuring is a syntax that lets you unpack values from arrays or properties from objects into distinct variables. Instead of accessing properties one at a time, you extract what you need in a single statement.
// Without destructuring
const user = { name: 'Alice', age: 30, role: 'admin' };
const name = user.name;
const age = user.age;
const role = user.role;
// With destructuring
const { name, age, role } = user;
Same result, one line instead of three. But destructuring goes much deeper than simple extraction โ it handles nested objects, arrays, defaults, renaming, and rest patterns.
Object Destructuring
Basic Syntax
Extract properties by name. The variable names must match the property names.
const config = {
host: 'localhost',
port: 3000,
debug: true,
};
const { host, port, debug } = config;
console.log(host); // 'localhost'
console.log(port); // 3000
console.log(debug); // true
Order doesn't matter โ it's matched by name, not position:
const { debug, host, port } = config; // same result
Default Values
If a property is undefined, a default kicks in:
const options = { color: 'blue' };
const { color, size = 'medium', visible = true } = options;
console.log(color); // 'blue' โ from object
console.log(size); // 'medium' โ default
console.log(visible); // true โ default
Defaults only apply to undefined, not null:
const { a = 10, b = 20 } = { a: null, b: undefined };
console.log(a); // null โ not undefined, so default doesn't apply
console.log(b); // 20 โ undefined triggers the default
Renaming (Aliasing)
Use : to assign to a different variable name:
const apiResponse = {
user_name: 'Alice',
user_age: 30,
is_active: true,
};
const {
user_name: userName,
user_age: userAge,
is_active: isActive,
} = apiResponse;
console.log(userName); // 'Alice'
console.log(userAge); // 30
console.log(isActive); // true
// console.log(user_name); // ReferenceError โ original name not created
Renaming + Defaults
You can combine renaming and defaults:
const data = { old_name: 'Alice' };
const {
old_name: name = 'Unknown',
old_email: email = 'no-email',
} = data;
console.log(name); // 'Alice' โ renamed from old_name
console.log(email); // 'no-email' โ not in object, uses default
Read it as: "take old_name, call it name, default to 'Unknown'."
Nested Object Destructuring
Dig into nested objects:
const response = {
data: {
user: {
id: 1,
profile: {
firstName: 'Alice',
lastName: 'Smith',
address: {
city: 'Portland',
state: 'OR',
},
},
},
},
status: 200,
};
const {
data: {
user: {
id,
profile: {
firstName,
lastName,
address: { city, state },
},
},
},
status,
} = response;
console.log(firstName); // 'Alice'
console.log(city); // 'Portland'
console.log(status); // 200
// console.log(data); // ReferenceError โ 'data' is not a variable
// console.log(profile); // ReferenceError โ intermediate names aren't bound
Important: intermediate names (data, user, profile, address) are not created as variables. Only the leaf names are. If you need both the nested value and the parent object, destructure separately:
const { data } = response;
const { user } = data;
const { profile: { firstName } } = user;
Computed Property Names
Use bracket notation to destructure dynamic keys:
const key = 'name';
const obj = { name: 'Alice', age: 30 };
const { [key]: value } = obj;
console.log(value); // 'Alice'
// Useful in functions
function getProperty(obj, prop) {
const { [prop]: value } = obj;
return value;
}
getProperty({ x: 10, y: 20 }, 'x'); // 10
Array Destructuring
Basic Syntax
Array destructuring works by position, not by name:
const colors = ['red', 'green', 'blue'];
const [first, second, third] = colors;
console.log(first); // 'red'
console.log(second); // 'green'
console.log(third); // 'blue'
Skipping Elements
Use commas to skip values:
const [, , blue] = ['red', 'green', 'blue'];
console.log(blue); // 'blue'
const [first, , third] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(third); // 3
Default Values
Same as objects โ defaults apply when the value is undefined:
const [a = 10, b = 20, c = 30] = [1, 2];
console.log(a); // 1
console.log(b); // 2
console.log(c); // 30 โ no third element, uses default
Swap Variables
The classic trick โ no temp variable needed:
let x = 1;
let y = 2;
[x, y] = [y, x];
console.log(x); // 2
console.log(y); // 1
// Works with more than two
let a = 1, b = 2, c = 3;
[a, b, c] = [c, a, b];
// a = 3, b = 1, c = 2
Nested Array Destructuring
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
const [[a, , c], , [g]] = matrix;
console.log(a); // 1
console.log(c); // 3
console.log(g); // 7
Destructuring from Functions
Very common pattern โ functions return arrays or objects, destructuring extracts the values:
// Array return (like React hooks)
function useState(initial) {
let value = initial;
const setter = (newVal) => { value = newVal; };
return [value, setter];
}
const [count, setCount] = useState(0);
// Object return (like API responses)
function fetchUser() {
return { data: { name: 'Alice' }, error: null, loading: false };
}
const { data, error, loading } = fetchUser();
Rest Patterns
Rest in Objects (...rest)
Collect remaining properties into a new object:
const user = { name: 'Alice', age: 30, role: 'admin', email: '[email protected]' };
const { name, ...rest } = user;
console.log(name); // 'Alice'
console.log(rest); // { age: 30, role: 'admin', email: '[email protected]' }
This is great for removing a property without mutating the original:
// Remove 'password' from user object
const userWithPassword = { id: 1, name: 'Alice', password: 'secret123' };
const { password, ...safeUser } = userWithPassword;
console.log(safeUser); // { id: 1, name: 'Alice' } โ no password
// userWithPassword still has password โ not mutated
Rest in Arrays
Collect remaining elements into a new array:
const [first, second, ...remaining] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(second); // 2
console.log(remaining); // [3, 4, 5]
Rest must be the last element:
// SyntaxError: Rest element must be last element
const [...rest, last] = [1, 2, 3];
Rest in Function Parameters
function logFirst(first, ...others) {
console.log('First:', first);
console.log('Others:', others);
}
logFirst('a', 'b', 'c', 'd');
// First: a
// Others: ['b', 'c', 'd']
// Combine with destructuring
function createUser({ name, email, ...metadata }) {
return {
name,
email,
metadata, // everything else
createdAt: Date.now(),
};
}
createUser({ name: 'Alice', email: '[email protected]', role: 'admin', dept: 'eng' });
// {
// name: 'Alice',
// email: '[email protected]',
// metadata: { role: 'admin', dept: 'eng' },
// createdAt: ...
// }
Spread Operator
Spread (...) looks like rest but does the opposite โ it expands an iterable into individual elements.
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Context โ Behavior โ
โโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Rest (left) โ const { a, ...rest } = obj โ collects โ
โ Spread (right)โ const copy = { ...obj } โ expands โ
โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Spread in Arrays
const a = [1, 2, 3];
const b = [4, 5, 6];
// Combine arrays
const combined = [...a, ...b];
// [1, 2, 3, 4, 5, 6]
// Insert in the middle
const inserted = [0, ...a, 3.5, ...b, 7];
// [0, 1, 2, 3, 3.5, 4, 5, 6, 7]
// Copy an array (shallow)
const copy = [...a];
copy.push(99);
console.log(a); // [1, 2, 3] โ original unchanged
console.log(copy); // [1, 2, 3, 99]
// Convert iterable to array
const chars = [...'hello'];
// ['h', 'e', 'l', 'l', 'o']
const unique = [...new Set([1, 2, 2, 3, 3])];
// [1, 2, 3]
// Spread into function arguments
const nums = [5, 2, 8, 1, 9];
Math.max(...nums); // 9 (same as Math.max(5, 2, 8, 1, 9))
Spread in Objects
const defaults = {
theme: 'dark',
fontSize: 14,
language: 'en',
};
const userPrefs = {
fontSize: 18,
language: 'fr',
};
// Merge โ later spreads override earlier ones
const config = { ...defaults, ...userPrefs };
// { theme: 'dark', fontSize: 18, language: 'fr' }
// Add/override specific properties
const updated = { ...config, theme: 'light', newProp: true };
// { theme: 'light', fontSize: 18, language: 'fr', newProp: true }
Order matters โ last one wins:
const a = { x: 1, y: 2 };
const b = { y: 3, z: 4 };
const result1 = { ...a, ...b }; // { x: 1, y: 3, z: 4 } โ b's y wins
const result2 = { ...b, ...a }; // { x: 1, y: 2, z: 4 } โ a's y wins
Conditional Spread
Conditionally include properties:
const isAdmin = true;
const includeDebug = false;
const config = {
name: 'App',
...(isAdmin && { adminPanel: true, adminTools: ['users', 'logs'] }),
...(includeDebug && { debug: true, verbose: true }),
};
// { name: 'App', adminPanel: true, adminTools: ['users', 'logs'] }
// debug/verbose not included because includeDebug is false
Warning: if the condition is false, false && { ... } evaluates to false, and ...false spreads nothing (it's a no-op). But 0 && { ... } also evaluates to 0, and ...0 will throw. Use a ternary for safety:
// Safer pattern
const config = {
name: 'App',
...(isAdmin ? { adminPanel: true } : {}),
};
Destructuring in Function Parameters
One of the most common uses โ clean up function signatures:
// Without destructuring โ what does each parameter mean?
function createUser(name, email, age, role, active) {
// ...
}
createUser('Alice', '[email protected]', 30, 'admin', true);
// With destructuring โ self-documenting
function createUser({ name, email, age = 0, role = 'user', active = true }) {
return { name, email, age, role, active, createdAt: Date.now() };
}
createUser({ name: 'Alice', email: '[email protected]', role: 'admin' });
// age defaults to 0, active defaults to true
Optional Parameter Object
Make the entire options object optional:
function connect({ host = 'localhost', port = 3000, ssl = false } = {}) {
console.log(`Connecting to ${host}:${port} (SSL: ${ssl})`);
}
connect(); // localhost:3000 (SSL: false)
connect({ port: 8080 }); // localhost:8080 (SSL: false)
connect({ ssl: true }); // localhost:3000 (SSL: true)
The = {} at the end means "if no argument is passed, use an empty object" so the destructuring doesn't throw on undefined.
Mixed Destructuring in Parameters
function processOrder(
orderId,
{ customer: { name, email }, items, shipping: { method = 'standard' } = {} }
) {
console.log(`Order ${orderId} for ${name} (${email})`);
console.log(`${items.length} items, shipping: ${method}`);
}
processOrder(123, {
customer: { name: 'Alice', email: '[email protected]' },
items: ['book', 'pen'],
shipping: { method: 'express' },
});
Shallow Copy Gotchas
Both spread and rest create shallow copies. Nested objects and arrays are shared references, not independent copies.
const original = {
name: 'Alice',
scores: [95, 87, 92],
address: {
city: 'Portland',
state: 'OR',
},
};
const copy = { ...original };
// Top-level properties are independent
copy.name = 'Bob';
console.log(original.name); // 'Alice' โ unchanged
// Nested objects are SHARED references
copy.scores.push(100);
console.log(original.scores); // [95, 87, 92, 100] โ MUTATED!
copy.address.city = 'Seattle';
console.log(original.address.city); // 'Seattle' โ MUTATED!
Shallow copy:
original โโโบ { name: 'Alice', scores: โโโบ [95, 87, 92], address: โโโบ { city: ... } }
โฒ โฒ
copy โโโโโบ { name: 'Bob', scores: โโ address: โโ }
copy.name is a separate string (primitive = copied by value)
copy.scores points to the SAME array (object = copied by reference)
copy.address points to the SAME object
Solutions for Deep Copy
// 1. structuredClone (modern, built-in)
const deepCopy = structuredClone(original);
deepCopy.scores.push(100);
console.log(original.scores.length); // 3 โ unchanged
// 2. JSON round-trip (older approach, has limitations)
const deepCopy2 = JSON.parse(JSON.stringify(original));
// Loses: functions, undefined, Date objects, RegExp, Map, Set, etc.
// 3. Manual nested spread (tedious but precise)
const manualCopy = {
...original,
scores: [...original.scores],
address: { ...original.address },
};
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Method โ Notes โ
โโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ { ...obj } โ Shallow only โ
โ Object.assign({},obj)โ Shallow only (same as spread) โ
โ JSON parse/stringify โ Deep, but loses functions, Dates, โ
โ โ undefined, Infinity, NaN, etc. โ
โ structuredClone() โ Deep, handles most types, no funcs โ
โ lodash.cloneDeep โ Deep, handles edge cases โ
โ Manual nested spread โ Precise control, verbose โ
โโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Array Shallow Copy Trap
const matrix = [[1, 2], [3, 4]];
const copy = [...matrix];
copy[0].push(99);
console.log(matrix[0]); // [1, 2, 99] โ inner array is shared!
// Fix
const deepMatrix = matrix.map((row) => [...row]);
deepMatrix[0].push(99);
console.log(matrix[0]); // [1, 2] โ safe
Real-World Patterns
API Response Handling
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
const { data: user, meta: { requestId } } = await response.json();
return { user, requestId };
}
const { user, requestId } = await getUser(42);
React Component Props
function Card({ title, children, className = '', onClick, ...rest }) {
return (
<div
className={`card ${className}`}
onClick={onClick}
{...rest} // spread remaining props onto the DOM element
>
<h2>{title}</h2>
{children}
</div>
);
}
// Usage โ any extra props (id, style, data-*) pass through
<Card title="Hello" className="featured" id="main-card" data-testid="card">
<p>Content</p>
</Card>
Configuration Merging
function createApp(userConfig = {}) {
const defaultConfig = {
port: 3000,
host: 'localhost',
cors: {
origin: '*',
methods: ['GET', 'POST'],
credentials: false,
},
logging: {
level: 'info',
format: 'json',
},
};
// Shallow merge won't work for nested objects
// Need to merge each level explicitly
const config = {
...defaultConfig,
...userConfig,
cors: {
...defaultConfig.cors,
...userConfig.cors,
},
logging: {
...defaultConfig.logging,
...userConfig.logging,
},
};
return config;
}
const app = createApp({
port: 8080,
cors: { credentials: true },
logging: { level: 'debug' },
});
// {
// port: 8080,
// host: 'localhost',
// cors: { origin: '*', methods: ['GET', 'POST'], credentials: true },
// logging: { level: 'debug', format: 'json' },
// }
State Updates (Immutable Pattern)
const state = {
user: { name: 'Alice', preferences: { theme: 'dark' } },
items: [
{ id: 1, text: 'Learn JS', done: false },
{ id: 2, text: 'Build app', done: false },
],
};
// Update nested property immutably
const newState = {
...state,
user: {
...state.user,
preferences: {
...state.user.preferences,
theme: 'light',
},
},
};
// Toggle a todo item immutably
const toggledState = {
...state,
items: state.items.map((item) =>
item.id === 1 ? { ...item, done: !item.done } : item
),
};
// Add an item immutably
const addedState = {
...state,
items: [...state.items, { id: 3, text: 'Deploy', done: false }],
};
// Remove an item immutably
const removedState = {
...state,
items: state.items.filter((item) => item.id !== 2),
};
Destructuring in Loops
const entries = [
['name', 'Alice'],
['age', 30],
['role', 'admin'],
];
for (const [key, value] of entries) {
console.log(`${key}: ${value}`);
}
// With Object.entries
const user = { name: 'Alice', age: 30, role: 'admin' };
for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`);
}
// Destructuring in map
const users = [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 },
];
const descriptions = users.map(({ name, age }) => `${name} is ${age}`);
// ['Alice is 30', 'Bob is 25']
Pattern: Extract + Transform
function formatAddress({ street, city, state, zip, country = 'US' }) {
return `${street}\n${city}, ${state} ${zip}\n${country}`;
}
const user = {
name: 'Alice',
address: {
street: '123 Main St',
city: 'Portland',
state: 'OR',
zip: '97201',
},
};
const formatted = formatAddress(user.address);
Common Mistakes
Declaring Without Keyword
When destructuring outside a declaration, wrap in parentheses:
let name, age;
// BAD โ JS thinks { starts a block
{ name, age } = { name: 'Alice', age: 30 }; // SyntaxError
// GOOD โ parentheses prevent block interpretation
({ name, age } = { name: 'Alice', age: 30 });
Missing Nested Guard
Destructuring throws if a nested path hits undefined or null:
const data = { user: null };
// TypeError: Cannot destructure property 'name' of null
const { user: { name } } = data;
// Fix: provide default for the nested object
const { user: { name } = {} } = data;
console.log(name); // undefined (safe)
// Or use optional chaining instead
const name = data.user?.name;
Confusing Rest with Spread
// REST โ collects into variable (left side of =)
const { a, ...rest } = { a: 1, b: 2, c: 3 };
// SPREAD โ expands into expression (right side of =)
const merged = { ...rest, d: 4 };
// They look the same (...) but do opposite things
// based on which side of the assignment they're on
Key Takeaways
- Object destructuring matches by name โ order doesn't matter
- Array destructuring matches by position โ order matters
- Defaults only trigger on undefined โ not null, not 0, not ''
- Rename with colon โ
{ oldName: newName }createsnewNamenotoldName - Rest must be last โ
{ a, ...rest }works,{ ...rest, a }doesn't - Spread creates shallow copies โ nested objects are shared references
- Use structuredClone for deep copies โ or manual nested spread for precision
- Destructure function parameters โ self-documenting, supports defaults
- Conditional spread โ
...(condition ? { prop: val } : {})for safe conditional inclusion - Parentheses for reassignment โ
({ a } = obj)when destructuring without let/const