Destructuring assignment
Objects and arrays are the two containers you reach for most in JavaScript. An object stores values behind named keys. An array lines values up in order. Both are great for holding data, but the moment you want to work with individual pieces, reaching in by arr[0] or obj.title over and over gets noisy.
Destructuring assignment is syntax that pulls values out of an array or object and drops them straight into variables. One line, several variables, no repeated indexing. It shines when a function takes a pile of parameters, half of them optional with defaults — you’ll see that pattern before the end.
firstName = “Maya”
surname = “Vance”
Array destructuring
Here’s an array unpacked into two variables:
// an array holding a name and a surname
let arr = ["Maya", "Vance"]
// destructuring assignment
// firstName gets arr[0]
// surname gets arr[1]
let [firstName, surname] = arr;
alert(firstName); // Maya
alert(surname); // Vance
From here on you work with firstName and surname directly, no bracket indexing required.
The pattern gets even nicer next to any method that hands you back an array, like split:
let [firstName, surname] = "Maya Vance".split(' ');
alert(firstName); // Maya
alert(surname); // Vance
Type any full name below and watch split(' ') feed a two-variable pattern. Edit the code to add a ...rest and capture middle names too.
The shape is small, but there are a handful of quirks that trip people up. The examples below walk through each one.
Click Swap and see the two roles trade values through a single destructuring line — no temporary variable in sight.
The rest ‘…’
When the array is longer than your list of variables, the leftover items simply have nowhere to go. Only what you name gets captured:
let [name1, name2] = ["Sofia", "Marlowe", "Captain", "of the Northern Fleet"];
alert(name1); // Sofia
alert(name2); // Marlowe
// the rest isn't assigned anywhere
If you do want the tail end, add a final variable prefixed with three dots. It scoops up everything that’s left into a real array:
let [name1, name2, ...rest] = ["Sofia", "Marlowe", "Captain", "of the Northern Fleet"];
// rest holds everything from index 2 onward
alert(rest[0]); // Captain
alert(rest[1]); // of the Northern Fleet
alert(rest.length); // 2
rest is an ordinary array of the remaining elements. The name is up to you — ...titles works identically — but it has two hard rules: the three dots must lead the name, and it has to come last in the pattern.
let [name1, name2, ...titles] = ["Sofia", "Marlowe", "Captain", "of the Northern Fleet"];
// titles = ["Captain", "of the Northern Fleet"]
Default values
Go the other direction — more variables than array items — and there’s no error. The unmatched variables just come out undefined:
let [firstName, surname] = [];
alert(firstName); // undefined
alert(surname); // undefined
To supply a fallback for a missing slot, write = after the variable name:
// default values
let [name = "Guest", surname = "Anonymous"] = ["Sofia"];
alert(name); // Sofia (array had a value here)
alert(surname); // Anonymous (default kicked in)
Defaults aren’t limited to constants. They can be any expression, including a function call. The key detail: a default is evaluated only when its slot is actually missing. If the array already provides a value, the default expression never runs.
// prompt runs only for surname, since name already has a value
let [name = prompt('name?'), surname = prompt('surname?')] = ["Sofia"];
alert(name); // Sofia (from array, prompt skipped)
alert(surname); // whatever you type into the prompt
That lazy evaluation matters when the default has a cost or a side effect. Here, the user is asked for a surname and nothing else, because name was already filled.
Object destructuring
Objects destructure too, and this is where the syntax earns its keep, since real code passes objects around constantly.
The shape:
let {var1, var2} = {var1:…, var2:…}
On the right, an existing object. On the left, a pattern wrapped in {...}. In the simplest form that pattern is just a list of names, and each name is matched against the property of the same name.
let card = {
title: "Update",
width: 100,
height: 200
};
let {title, width, height} = card;
alert(title); // Update
alert(width); // 100
alert(height); // 200
card.title, card.width, and card.height flow into the matching variables.
Because matching happens by name and not by position, order is irrelevant. This produces the same result:
// order shuffled — still matches by property name
let {height, width, title} = { title: "Update", height: 200, width: 100 }
Renaming: property : variable
Want the value under a property but stored in a differently named variable? Use a colon. Read it as “this property goes into that variable.” So sending card.width into a variable called w:
let card = {
title: "Update",
width: 100,
height: 200
};
// { sourceProperty: targetVariable }
let {width: w, height: h, title} = card;
// width -> w
// height -> h
// title -> title
alert(title); // Update
alert(w); // 100
alert(h); // 200
The colon points from the property name on the left to the variable name on the right. width lands in w, height in h, and title keeps its own name since we didn’t rename it.
Defaults for objects
Set a fallback with = for any property that might be absent:
let card = {
title: "Update"
};
let {width = 100, height = 200, title} = card;
alert(title); // Update
alert(width); // 100 (default)
alert(height); // 200 (default)
As with arrays, these defaults can be any expression — function calls included — and they run only when the property is missing:
let card = {
title: "Update"
};
let {width = prompt("width?"), title = prompt("title?")} = card;
alert(title); // Update (present, so no prompt)
alert(width); // whatever prompt returns
Colon and equals combine freely: rename and provide a default in one clause.
let card = {
title: "Update"
};
let {width: w = 100, height: h = 200, title} = card;
alert(title); // Update
alert(w); // 100
alert(h); // 200
Read width: w = 100 as “take property width, put it in variable w, and if width is missing use 100.”
From a big object you can grab only the one property you care about and ignore the rest:
let card = {
title: "Update",
width: 100,
height: 200
};
// pull out only title
let { title } = card;
alert(title); // Update
The rest pattern “…”
When the object has more properties than variables, the rest pattern gathers the leftovers into a new object — the object-world version of the array ...rest:
let card = {
title: "Update",
height: 200,
width: 100
};
// title = the title property
// rest = an object holding every property not named above
let {title, ...rest} = card;
// title = "Update", rest = { height: 200, width: 100 }
alert(rest.height); // 200
alert(rest.width); // 100
rest is a genuine object with the untouched properties as its own keys. It works in every current runtime; very old environments (such as legacy IE) need a transpiler like Babel to support it.
Nested destructuring
When a container holds other containers, the pattern can mirror that structure to reach deep values. Match arrays with [...] and objects with {...}, nested to whatever depth you need.
Here card carries a size object and a tags array, and the left side echoes that layout:
let card = {
size: {
width: 100,
height: 200
},
tags: ["news", "changelog"],
extra: true
};
// pattern split across lines for readability
let {
size: { // descend into size
width,
height
},
tags: [tag1, tag2], // unpack the tags array
title = "Update" // absent in card, so the default is used
} = card;
alert(title); // Update
alert(width); // 100
alert(height); // 200
alert(tag1); // news
alert(tag2); // changelog
Every property except extra — which the pattern never mentions — ends up in a variable.
Notice there’s no size variable and no tags variable. Those names only appear as waypoints in the pattern — you drilled through them to reach width, height, tag1, and tag2, so the intermediate objects themselves never get bound.
Smart function parameters
Some functions accept a long list of parameters where most are optional. UI code is full of them. Picture a function that builds a card: it might take a title, a width, a height, a list of tags, and more.
The clumsy approach lines them all up positionally:
function buildCard(title = "Untitled", width = 200, height = 100, tags = []) {
// ...
}
Two problems show up fast. First, you have to remember the exact order — a good editor helps, but you’re still counting commas. Second, calling it when you only want to override one late parameter is painful; you have to pad the earlier ones with undefined to keep the positions aligned:
// undefined placeholders just to reach the tags argument
buildCard("Release notes", undefined, undefined, ["news", "changelog"])
That’s fragile and hard to read, and it only gets worse with more parameters.
Destructuring fixes it. Accept a single object and destructure it right in the parameter list. The caller passes named properties in any order, and defaults cover anything left out:
// caller passes one object
let card = {
title: "Release notes",
tags: ["news", "changelog"]
};
// the function destructures it on the way in
function buildCard({title = "Untitled", width = 200, height = 100, tags = []}) {
// title and tags came from card,
// width and height fell back to defaults
alert( `${title} ${width} ${height}` ); // Release notes 200 100
alert( tags ); // news, changelog
}
buildCard(card);
The full pattern is available here too — nested destructuring, colon renames, defaults, all of it:
let card = {
title: "Release notes",
tags: ["news", "changelog"]
};
function buildCard({
title = "Untitled",
width: w = 100, // width goes into w
height: h = 200, // height goes into h
tags: [tag1, tag2] // first tag into tag1, second into tag2
}) {
alert( `${title} ${w} ${h}` ); // Release notes 100 200
alert( tag1 ); // news
alert( tag2 ); // changelog
}
buildCard(card);
The syntax is identical to a standalone destructuring assignment:
function({
incomingProperty: varName = defaultValue
...
})
You get a variable varName holding the value of incomingProperty, falling back to defaultValue when that property isn’t supplied.
Don’t forget the empty-object default
There’s a catch. Destructuring a parameter assumes an argument is actually there to destructure. Call the function with nothing and it breaks:
buildCard({}); // fine — an empty object, every default applies
buildCard(); // TypeError — nothing to destructure
Calling buildCard() passes undefined as the argument, and you can’t read properties off undefined. The fix is to give the whole parameter its own default of {}:
function buildCard({ title = "Update", width = 100, height = 200 } = {}) {
alert( `${title} ${width} ${height}` );
}
buildCard(); // Update 100 200
Now when no argument arrives, the parameter defaults to an empty object, there’s always something to destructure, and the inner defaults fill every field.
Toggle which properties you send below. Anything you leave off falls back to its default, and unchecking everything is the same as buildCard() — the = {} keeps it from crashing.
Summary
-
Destructuring assignment maps an object or array onto many variables in a single, readable statement, without mutating the source.
-
Full object syntax:
let {prop : varName = defaultValue, ...rest} = objectProperty
propgoes into variablevarName; if that property is absent,defaultValueis used. Any properties the pattern didn’t name are collected into therestobject. -
Full array syntax:
let [item1 = defaultValue, item2, ...rest] = arrayPosition 0 goes into
item1, position 1 intoitem2, and everything remaining becomes the arrayrest. -
Objects match by name (order-free), arrays match by position.
-
Defaults can be expressions or function calls, and run only when their value is missing.
-
Patterns nest: mirror the shape of the source on the left to pull values out of nested arrays and objects.
-
Destructured function parameters replace long positional argument lists — add a
= {}default on the parameter so the function still works when called with no arguments.