JavaScript (ES2026) Module 5 – Objects in depth
Destructuring objects and arrays
Unpack values from JavaScript objects and arrays with destructuring, with defaults, renaming, nested patterns, rest, swaps and clearer function parameters.
What you will learn
- Extract values from objects and arrays with destructuring
- Use defaults, renaming, nested patterns and rest elements
- Write function parameters that destructure an options object for readable APIs
Before you start
What you’ll learn · Before you start
What you will learn
- Extract values from objects and arrays with destructuring
- Use defaults, renaming, nested patterns and rest elements
- Write function parameters that destructure an options object for readable APIs
Before you start
On this page
Destructuring unpacks values from an object or an array into variables with a pattern that mirrors the data’s
shape. const { city, temp } = weather creates the variables city and temp from the properties of the same names,
and const [first, second] = queue takes the first two items by position. Patterns can rename ({ temp: celsius }),
supply defaults ({ alerts = [] }), reach into nested objects ({ current: { humidity } }), skip items
([, second]) and collect the rest ({ ...others }, [head, ...tail]). They also work in function parameters,
which turns an options object into readable named arguments. Three rules prevent most surprises: a default applies
only when the value is undefined, never null; destructuring null or undefined, or reading inside a nested
object that is missing, throws a TypeError; and array patterns accept any iterable. This lesson reads an API
response, works through defaults, arrays, swaps, missing data and parameters, and ends with an address formatter.
Object patterns
// Reading the fields you need from a weather API response with one destructuring pattern.
const response = JSON.parse(`{
"city": "Pune",
"current": { "temp": 31.4, "humidity": 62, "wind": { "speed": 11, "dir": "W" } },
"units": "metric"
}`);
const {
city, // the property city, into a variable called city
current: { temp: celsius, humidity, wind: { speed } }, // nested, with temp renamed to celsius
alerts = [], // missing from the response, so the default is used
...rest // every other own property: { units: 'metric' }
} = response;
console.log(city, celsius, humidity, speed, alerts, rest);
// A computed key reads the property named in a variable.
const field = 'humidity';
const { [field]: reading } = response.current;
console.log(`${field}: ${reading}`);
// The same reads without destructuring, for comparison.
const sameCity = response.city;
const sameCelsius = response.current.temp;
console.log(sameCity === city, sameCelsius === celsius); Output
Pune 31.4 62 11 [] { units: 'metric' }
humidity: 62
true true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node api-destructure.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
A destructuring pattern matched against a response
Text description of the diagram
The diagram has three boxes from top to bottom.
- The response object has `city: 'Pune'`, a nested `current` object with `temp: 31.4` and `humidity: 62`, and `units: 'metric'`.
- The pattern `{ city, current: { temp: celsius, humidity }, alerts = [], ...rest }` is matched against it by key.
- It creates five variables: `city` is `'Pune'`; `celsius` is `31.4`, read from `current.temp` and renamed; `humidity` is `62`; `alerts` is `[]`, its default, because the response has no `alerts`; and `rest` is `{ units: 'metric' }`, the properties no other part of the pattern named. No variable called `current` is created: the nested pattern only reads inside it.
Each part of the pattern does one job:
cityreadsresponse.cityinto a variable with the same name. This is the common case: the variable is named after the property.current: { temp: celsius, humidity, wind: { speed } }is a nested pattern. The part before the colon names the property to read; the part after it says what to do with the value, here destructure it again.temp: celsiusrenames: it readstempinto a variable calledcelsius. No variable calledcurrentorwindis created.alerts = []is a default. The response has noalerts, so the variable gets an empty array instead ofundefined....restcollects every own property the pattern did not name into a new object, here{ units: 'metric' }. It must come last.{ [field]: reading }uses a computed key, so the property to read can come from a variable.
The last line confirms that destructuring is only shorter syntax for property reads: city is exactly
response.city. The variables are copies of the values, so for nested objects they hold references to the same
objects, as with any assignment.
Defaults apply only to undefined
// A default applies only when the value is undefined: missing, or undefined itself.
const { qty = 1 } = { qty: null };
const { size = 'M' } = {};
const { colour = 'blue' } = { colour: undefined };
console.log(qty, size, colour);
// Defaults are evaluated only when they are needed, from left to right, and may use earlier names.
let lookups = 0;
function lookUpCity() {
lookups += 1;
return 'Mumbai';
}
const { city = lookUpCity(), label = `Weather in ${city}` } = { city: 'Pune' };
console.log(label, '| lookups:', lookups);
// When null also means "no value", ?? treats null and undefined alike.
const saved = { qty: null };
const { qty: fromPattern = 1 } = saved;
const fromNullish = saved.qty ?? 1;
console.log(fromPattern, fromNullish); Output
null M blue Weather in Pune | lookups: 0 null 1
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node defaults.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The specification checks for one value: if the value read is undefined, the default is evaluated and used.
{ qty: null } therefore keeps null, while a missing size and an explicit undefined both get their defaults.
Data from JSON is the usual trap, because JSON has null but no undefined: an API that sends "qty": null defeats a
default of 1. When null should fall back as well, use ??, which treats null and undefined alike.
Defaults are expressions that run only when they are needed, from left to right. lookUpCity() never ran, because
city had a value, and label could use city because it comes later in the pattern.
Array patterns
// Array patterns take values by position.
const queue = ['Asha', 'Ravi', 'Meera', 'John'];
const [first, second, ...waiting] = queue;
console.log(first, second, waiting);
const [, , third = 'nobody'] = ['A1', 'A2']; // skip two places; there is no third value, so the default is used
console.log(third);
// Swapping two variables needs no temporary variable.
let left = 'tea';
let right = 'coffee';
[left, right] = [right, left];
console.log(left, right);
// Any iterable works: a string, a Set, a Map, a generator.
const [initial] = 'Asha';
const [firstTag, secondTag] = new Set(['new', 'sale', 'new']);
const [[firstKey, firstValue]] = new Map([['pen', 20]]);
console.log(initial, firstTag, secondTag, firstKey, firstValue);
function* ticketNumbers() {
let n = 1;
while (true) yield `T${n++}`; // never ends on its own
}
const [t1, t2] = ticketNumbers(); // takes two values, then stops the generator
console.log(t1, t2); Output
Asha Ravi [ 'Meera', 'John' ] nobody coffee tea A new sale pen 20 T1 T2
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node swap-and-rest.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Array patterns match by position instead of by name:
[first, second, ...waiting]takes the first two items and collects the remaining ones into a new array.- A hole,
[, , third], skips a position. A default works as with objects: there is no third item, sothirdis'nobody'. [left, right] = [right, left]swaps two variables. It builds a temporary array on the right and destructures it on the left, so no third variable is needed.- An array pattern accepts any iterable, because it reads values with the iteration protocol: a string gives
characters, a Set gives its values without duplicates, a Map gives
[key, value]pairs (destructured again here), and a generator gives values on demand. The pattern takes only what it needs and then closes the iterator, which is why the endlessticketNumbers()generator is safe here.
Objects that are not iterable cannot be destructured with an array pattern: const [x] = {} throws a TypeError.
One syntax trap comes with destructuring assignment to existing variables. In code without semicolons, a line
that starts with [ is joined to the line before it:
// Code written without semicolons: a line that starts with [ continues the line before it.
const broken = new Function(`
let a = 1
let b = 2
const pair = [a, b]
[a, b] = [b, a]
return { a, b, pair }
`);
console.log('without a semicolon:', broken());
const fixed = new Function(`
let a = 1
let b = 2
const pair = [a, b]
;[a, b] = [b, a]
return { a, b, pair }
`);
console.log('with ;[ at the start:', fixed()); Output
without a semicolon: { a: 1, b: 2, pair: [ 2, 1 ] }
with ;[ at the start: { a: 2, b: 1, pair: [ 1, 2 ] }
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node asi-swap.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Without the semicolon, const pair = [a, b] and the next line are read as const pair = [a, b][a, b] = [b, a],
which assigns to an element of a temporary array: a and b stay as they were, pair becomes [2, 1], and no
error appears. Starting the line with ;[ (or ending the previous one with ;) fixes it. An object pattern assigned
to existing variables has its own rule: wrap the statement in parentheses, ({ a, b } = source), because a line that
starts with { is read as a block.
When the data is missing
Destructuring reads properties, so it fails where a property read fails:
// A nested pattern reads inside an inner object, so the inner object has to be there.
const withAddress = { name: 'Asha', address: { city: 'Pune', pin: '411001' } };
const withoutAddress = { name: 'Ravi' };
const nullAddress = { name: 'Meera', address: null };
const cityOf = ({ address: { city } }) => city;
console.log(cityOf(withAddress));
try {
cityOf(withoutAddress);
} catch (error) {
console.log('missing address:', error.name);
}
// A default for the inner object covers a missing one, but not null: defaults apply only to undefined.
const safeCityOf = ({ address: { city } = {} }) => city;
console.log(safeCityOf(withoutAddress));
try {
safeCityOf(nullAddress);
} catch (error) {
console.log('null address:', error.name);
}
// For data that may hold null, optional chaining with ?? is often the clearer tool.
const cityOrUnknown = (person) => person.address?.city ?? 'unknown';
console.log(cityOrUnknown(nullAddress), cityOrUnknown(withAddress));
// Destructuring null or undefined itself always throws.
try {
const { name } = null;
console.log(name);
} catch (error) {
console.log('destructuring null:', error.name);
} Output
Pune missing address: TypeError undefined null address: TypeError unknown Pune destructuring null: TypeError
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node nested-missing.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
({ address: { city } })throws aTypeErrorfor Ravi, who has no address: the nested pattern tries to readcityfromundefined.- A default for the nested object,
{ address: { city } = {} }, handles a missing address, andcityis simplyundefined. - That default does nothing for Meera, whose address is
null, because defaults apply only toundefined. Optional chaining with??,person.address?.city ?? 'unknown', handles both cases and is often the clearer choice for data that may holdnull. - Destructuring
nullorundefineditself,const { name } = null, always throws.
Destructuring parameters
A function can destructure its argument directly in the parameter list. This is the usual way to write a function that takes an options object:
// Destructuring in a parameter list: named options, defaults, and a call with no argument at all.
function formatTicket({ train, coach = 'GEN', seat, passenger: { name } = {} } = {}) {
return `${train ?? 'train ?'} · coach ${coach} · seat ${seat ?? '-'} · ${name ?? 'no name'}`;
}
console.log(formatTicket({ train: '12127', coach: 'B2', seat: 14, passenger: { name: 'Asha' } }));
console.log(formatTicket({ seat: 3, train: '22105' })); // any order; missing options get their defaults
console.log(formatTicket()); // the final = {} makes even a call with no argument safe
// The positional version needs the right order, and a placeholder for every option that is skipped.
function formatTicketByPosition(train, coach = 'GEN', seat, name) {
return `${train} · coach ${coach} · seat ${seat} · ${name}`;
}
console.log(formatTicketByPosition('12127', undefined, 14, 'Asha'));
// Destructuring in a loop: the [key, value] pairs of Object.entries.
const fares = { sleeper: 450, ac3: 1200 };
for (const [travelClass, fare] of Object.entries(fares)) console.log(`${travelClass}: ${fare}`); Output
12127 · coach B2 · seat 14 · Asha 22105 · coach GEN · seat 3 · no name train ? · coach GEN · seat - · no name 12127 · coach GEN · seat 14 · Asha sleeper: 450 ac3: 1200
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node params.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
- Callers pass named options in any order,
{ seat: 3, train: '22105' }, and every option that is left out gets its default orundefined. The positional version needs every argument in the right place and anundefinedfor each one that is skipped, which is hard to read and easy to get wrong. - The final
= {}is the default for the whole argument. Without it,formatTicket()would try to destructureundefinedand throw. - Nested defaults,
passenger: { name } = {}, let callers leave out whole groups of options. - The same patterns work in loops:
for (const [travelClass, fare] of Object.entries(fares))names both parts of each entry.
Destructuring does not change the object it reads, and the variables it creates are independent of the object
afterwards: assigning to city later does not change response.city.
Exercise: format an address that may have parts missing
Exercise · Easy · JavaScript
Format an address that may have parts missing
formatAddress(person) turns { name: 'Asha Rao', address: { line1: '12 MG Road', city: 'Pune', pin: '411001' } } into one line: 'Asha Rao, 12 MG Road, Pune 411001'. Real records are incomplete, and the version in format-address.mjs crashes or prints undefined for them. Use destructuring, with defaults where they help, so that it handles every case:
- The line is the name, the first address line and "city pin", joined with
', '. The city and the pin are joined with a single space. - A part that is missing,
undefined,nullor the empty string''is left out, together with its separator:{ name: 'Ravi', address: { city: 'Kolkata' } }gives'Ravi, Kolkata'. - A person with no address, or with
address: null, gives just the name. formatAddress()with no argument at all returns''.
Starter code · format-address.mjs
// formatAddress(person): one line for an address whose parts may be missing (the exercise of lesson
// javascript/destructuring). This version works only for complete records.
export function formatAddress({ name, address: { line1, city, pin } }) {
return `${name}, ${line1}, ${city} ${pin}`;
} The sample tests · format-address.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { formatAddress } from './format-address.mjs';
test('a complete record', () => {
const person = { name: 'Asha Rao', address: { line1: '12 MG Road', city: 'Pune', pin: '411001' } };
assert.equal(formatAddress(person), 'Asha Rao, 12 MG Road, Pune 411001');
});
test('missing parts are left out with their separators', () => {
assert.equal(formatAddress({ name: 'Ravi', address: { city: 'Kolkata' } }), 'Ravi, Kolkata');
assert.equal(formatAddress({ name: 'Ravi', address: { line1: '4 Park Street', pin: '700016' } }), 'Ravi, 4 Park Street, 700016');
assert.equal(formatAddress({ address: { line1: '4 Park Street', city: 'Kolkata' } }), '4 Park Street, Kolkata');
});
test('no address, or a null address, gives just the name', () => {
assert.equal(formatAddress({ name: 'Meera' }), 'Meera');
assert.equal(formatAddress({ name: 'Meera', address: null }), 'Meera');
});
test('null and empty strings count as missing', () => {
assert.equal(formatAddress({ name: '', address: { line1: null, city: 'Pune', pin: '' } }), 'Pune');
assert.equal(formatAddress({ name: 'John', address: { line1: '', city: '', pin: '560001' } }), 'John, 560001');
});
test('a call with no argument returns an empty string', () => {
assert.equal(formatAddress(), '');
assert.equal(formatAddress({}), '');
});
test('the record is not changed', () => {
const person = { name: 'Asha', address: { city: 'Pune' } };
formatAddress(person);
assert.deepEqual(person, { name: 'Asha', address: { city: 'Pune' } });
}); A hint
Destructure the parameter with a default for the whole argument, ({ name, address } = {}), so a call with no argument works. A default for address itself would not help with address: null, because defaults apply only to undefined: destructure the inner object from address ?? {} instead. Then collect the parts in an array and keep only the ones that are present before you join them.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Self-check
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · What is destructuring in JavaScript, and when does a default value in a pattern apply?
Destructuring is syntax that copies values out of an object or an array into variables with a pattern shaped like
the data: const { name, city } = user reads two properties by name, and const [first, second] = list reads two
items by position. Patterns can rename ({ city: town }), go into nested objects, skip array items with holes and
collect the remaining values with a rest element such as ...others. A default value, as in { qty = 1 }, is used only when the value read is undefined,
either because the property is missing or because it holds undefined. A null value is kept, which matters for JSON
data, where missing values often arrive as null; use ?? when null should fall back too. Destructuring null or
undefined itself, or a nested object that is missing, throws a TypeError.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A service reads its settings with const { timeoutMs = 5000, retries = 3 } = config. After the settings moved to a JSON file written by an admin tool, every request started timing out at once. The tool writes "timeoutMs": null when the field is left empty. Explain the chain of events and make loading the settings robust.
-
Mid to senior
A request handler starts with const { user: { id }, items: [firstItem] } = await res.json(). In production some requests fail with TypeErrors, and a teammate asks for "defensive destructuring". Which inputs make each part throw, why are defaults at every level not enough, and how would you rewrite it?
Key takeaways
- Object patterns read by name (
{ city, temp: celsius }), array patterns by position ([first, , third]); both support defaults, nesting and a final rest element. - A default applies only when the value is
undefined;nullis kept, so use??for data that sendsnull. - Destructuring
nullorundefined, or reading inside a missing nested object, throws aTypeError; give nested patterns a default= {}, or use optional chaining for data that may benull. - Array patterns accept any iterable (strings, Sets, Maps, generators) and stop the iterator once they have what they need.
[a, b] = [b, a]swaps; in code without semicolons, start such a line with;. Wrap an object assignment pattern in parentheses:({ a } = obj).- Destructured parameters with a final
= {}give functions readable, optional named options that callers can pass in any order.
References
- ECMAScript 2026 Language Specification: destructuring binding patterns (Ecma International)
- ECMAScript 2026 Language Specification: destructuring assignment (Ecma International)
- ECMAScript 2026 Language Specification: KeyedBindingInitialization (Ecma International)
- ECMAScript 2026 Language Specification: BindingInitialization (Ecma International)
- ECMAScript 2026 Language Specification: rules of automatic semicolon insertion (Ecma International)
- Destructuring (MDN Web Docs)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress