JavaScript Object Destructuring
Object destructuring is a JavaScript syntax that lets you unpack properties from an object directly into individual variables, using a pattern that mirrors the object’s shape. Instead of writing person.name, person.age, and person.city on separate lines, you can pull all three values out in a single, readable statement. Introduced in ES2015 (ES6), it has become one of the most heavily used features in modern JavaScript, especially for function arguments, imported modules, and API response handling. Mastering it will make your code shorter, clearer, and closer to how experienced JavaScript developers actually write today.
Overview / How It Works
Object destructuring works by name, not position. When you write const { age } = person, the engine looks for a property literally called age on the person object and binds its value to a new variable also called age. This is fundamentally different from array destructuring, which is positional (it grabs the first element, then the second, and so on regardless of any name). Because object destructuring is name-based, the order you list properties in the pattern does not need to match the order they were defined in the object literal.
Under the surface, destructuring is really just repeated property access performed automatically for you. For every binding in the pattern, the JavaScript engine performs a [[Get]] operation on the source object using that property’s key, exactly as if you had typed object.key yourself. If the source expression on the right-hand side is not an object (for example null or undefined), that first [[Get]] fails and the engine throws a TypeError immediately, before any variables are created.
Destructuring patterns can nest arbitrarily deep. If a property’s value is itself an object, you can open another { } pattern inside the outer one and the same name-based lookup rules apply recursively, one level at a time. You can also rename any extracted binding, supply a default value that only kicks in when the property is undefined (not merely falsy), and collect whatever properties you did not explicitly name into a fresh object using the rest pattern ...rest. Destructuring can appear in three places: variable declarations (const, let, or var), plain assignment expressions on existing variables, and function parameter lists — which is extremely common for functions that accept a single “options” object.
Syntax
const {
propertyName,
originalName: aliasName,
propertyWithDefault = defaultValue,
originalName2: aliasName2 = defaultValue2,
...restOfProperties
} = someObject;
| Pattern | Meaning |
|---|---|
{ propertyName } |
Reads someObject.propertyName and creates a variable of the same name. |
{ originalName: aliasName } |
Reads someObject.originalName but binds it to a differently named variable, aliasName. |
{ propertyWithDefault = defaultValue } |
Uses defaultValue only if the property is missing or is exactly undefined. |
{ [expression]: aliasName } |
Computed property name — the key to read is the result of evaluating expression at runtime. |
{ ...restOfProperties } |
Must come last; collects all remaining own enumerable properties into a new plain object. |
Examples
The first example shows the simplest case: pulling several top-level properties out of an object at once.
const person = { name: 'Ava', age: 28, city: 'Berlin' };
const { name, age, city } = person;
console.log(`${name} is ${age} years old and lives in ${city}.`);
Output:
Ava is 28 years old and lives in Berlin.
Here, name, age, and city are each looked up on person by key and assigned to new const variables with matching names, which are then used inside a template literal.
The second example combines renaming, nested destructuring, and a default value in one pattern.
const user = {
id: 42,
profile: {
displayName: 'nightowl99',
social: { twitter: '@nightowl99' }
}
};
const {
id: userId,
profile: {
displayName,
social: { twitter },
bio = 'No bio yet'
}
} = user;
console.log(userId);
console.log(displayName);
console.log(twitter);
console.log(bio);
Output:
42
nightowl99
@nightowl99
No bio yet
id is renamed to userId, while profile is never bound directly — instead its inner shape is destructured immediately, reaching two levels deep to grab twitter out of social. Since profile has no bio property, the default value 'No bio yet' is used.
The third example is a more realistic, function-oriented use case: parameter destructuring with a default, a rest pattern, and a computed property name.
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};
function createClient({ apiUrl, timeout = 3000, ...options }) {
console.log(`Client for ${apiUrl}, timeout=${timeout}ms`);
console.log(options);
}
createClient(config);
const settingKey = 'retries';
const { [settingKey]: retryCount } = config;
console.log(`Retries configured: ${retryCount}`);
Output:
Client for https://api.example.com, timeout=5000ms
{ retries: 3 }
Retries configured: 3
createClient destructures its single argument right in the parameter list, a very common pattern for “options object” style APIs. Because config.timeout is 5000 (not undefined), the default 3000 is ignored. Everything not explicitly named — here just retries — ends up in the options rest object. The final lines show a computed property name: the key to read, 'retries', is stored in the variable settingKey and evaluated at runtime inside the square brackets.
Under the Hood / Step by Step
Walking through the nested example above, the engine roughly does the following, in order:
- Evaluate the right-hand side once, producing a reference to the
userobject. This happens only a single time no matter how many properties the pattern extracts. - For the top-level pattern, perform
[[Get]]forid, obtaining42, and bind it to the new nameuserId. - Perform
[[Get]]forprofile, obtaining the nested object. Since the pattern forprofileis itself a destructuring pattern (not a simple binding), no variable calledprofileis ever created — the engine immediately recurses into it. - Inside that nested object, perform
[[Get]]fordisplayName,social, andbioin the order they’re written in the pattern.socialis destructured again to reachtwitter. - Because
biodoes not exist onprofile, its value isundefined, which triggers the default value expression, evaluating and binding'No bio yet'.
Two consequences fall directly out of this mechanism. First, if the right-hand side is null or undefined, the very first [[Get]] attempt fails at the engine level, so the whole statement throws a TypeError before any bindings are created — there is no partial destructuring. Second, because each property is located by key rather than position, you can freely reorder properties in the pattern without changing the result, unlike array destructuring where order is everything.
Common Mistakes
Mistake 1: Destructuring into existing variables without wrapping parentheses. When a statement begins with {, JavaScript’s parser assumes it’s the start of a block statement, not an object pattern — so an assignment like the one below is a syntax error, not a working reassignment.
let a, b;
{ a, b } = { a: 1, b: 2 };
console.log(a, b);
The fix is to wrap the whole assignment expression in parentheses, which tells the parser to treat the leading { as an object pattern rather than a block:
let a, b;
({ a, b } = { a: 1, b: 2 });
console.log(a, b);
Output:
1 2
Mistake 2: Assuming defaults apply to any falsy value. A default value is only used when the extracted property is exactly undefined — not for null, 0, '', or false. This surprises developers who expect defaults to behave like a general fallback.
const { color = 'red' } = { color: null };
console.log(color);
const { size = 'M' } = { size: undefined };
console.log(size);
Output:
null
M
The first destructure keeps null exactly as given, because null is a legitimate, explicit value — the default only exists to fill in for a missing or undefined value. If you need to treat null the same as “missing,” combine destructuring with the nullish coalescing operator (??) beforehand, or normalize the object before destructuring it.
Best Practices
- Use destructuring for function parameters when a function takes many related options — it documents the expected shape right in the signature.
- Give destructured defaults for optional parameters instead of writing manual
if (x === undefined)checks. - Rename (
propertyName: alias) when the source property’s name would collide with another variable already in scope, or when the original name is unclear in context. - Prefer nested destructuring over chains of dot access (
data.user.profile.name) when you need several values from the same nested object. - Use the rest pattern (
...rest) to separate “known” properties from everything else, such as when forwarding leftover props to another function or component. - Remember to wrap bare-object reassignments to existing variables in parentheses, since
{ ... } = objat the start of a statement is a syntax error. - Don’t destructure a value that might be
nullorundefinedwithout guarding it first (a default parameter, an??fallback, or a preceding check) — theTypeErrorhappens immediately, before any defaults inside the pattern can help.
Practice Exercises
- Given
const book = { title: 'Dune', author: 'Frank Herbert', year: 1965 };, use destructuring in a single statement to create variablestitleandyearonly, and log a sentence combining them. - Given
const response = { status: 200, data: { items: [1, 2, 3], total: 3 }, error: null };, destructurestatusand, from the nesteddataobject,total— renaming it tototalCount. Log both values. - Write a function
describeProductthat accepts a single object argument and destructuresname, apricewith a default of0, and collects any other properties into adetailsrest object. Call it with{ name: 'Mug', price: 12, color: 'blue', material: 'ceramic' }and log a summary line plus thedetailsobject.
Summary
- Object destructuring extracts properties by name, not position, so the order in the pattern doesn’t matter.
- You can rename bindings with
original: aliasand supply defaults with= value, which only apply when the property isundefined. - Patterns can nest to nab deeply nested values in one statement, and computed keys (
[expr]: alias) let you extract a property whose name is only known at runtime. - The rest pattern
...restgathers any leftover own enumerable properties into a new plain object, and must come last. - Destructuring a
nullorundefinedvalue throws aTypeErrorimmediately, since it relies on a property lookup under the hood. - Reassigning existing variables with a bare
{ ... } = objstatement requires wrapping parentheses to avoid a syntax error.
