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.

array
“Maya”“Vance”

firstName = “Maya”

surname = “Vance”

Destructuring pulls values out of a container and into named variables in one step.

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.

interactiveSplit a name into destructured variables

The shape is small, but there are a handful of quirks that trip people up. The examples below walk through each one.

0: “Sofia”1: “Marlowe”2: “Captain”3: “of the…”
firstName, (skip)rank← positions 3+ have no target
An empty comma reserves a position without binding it to a variable.

Click Swap and see the two roles trade values through a single destructuring line — no temporary variable in sight.

interactiveSwap two variables with destructuring

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"]
“Sofia”“Marlowe”“Captain”“of the Northern Fleet”
name1name2…rest → [ “Captain”, “of the Northern Fleet” ]
Named variables take the front of the array; ...rest collects the tail into a new array.

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 }
card
title: “Update”
width: 100
height: 200
height ← heightwidth ← widthtitle ← title
Object destructuring matches by property name, so order on the left doesn't matter.

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.

card
size: {
width: 100
height: 200
}
tags: [ “news”, “changelog” ]
extra: true ← ignored
width = 100height = 200tag1 = “news”tag2 = “changelog”title = “Update” (default)
The left-side pattern mirrors the shape of the source object to reach nested values.

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.

interactiveDestructured parameters with two layers of defaults

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} = object

    Property prop goes into variable varName; if that property is absent, defaultValue is used. Any properties the pattern didn’t name are collected into the rest object.

  • Full array syntax:

    let [item1 = defaultValue, item2, ...rest] = array

    Position 0 goes into item1, position 1 into item2, and everything remaining becomes the array rest.

  • 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.