JavaScript Destructuring

6 minute read

Destructuring pulls values out of arrays and properties out of objects in a single, declarative expression. It replaces dozens of const x = obj.x; const y = obj.y; lines with one. Once it clicks, it’s the syntax you’ll use a hundred times a day — in function parameters, in imports, in React props, anywhere data flows.

Array Destructuring

const [a, b, c] = [1, 2, 3];
// a = 1, b = 2, c = 3

const [first] = [10, 20, 30];
// first = 10  — extras ignored

const [, , third] = [10, 20, 30];
// third = 30  — skip with commas

const [x = 99] = [];
// x = 99  — default kicks in when value is undefined

const [head, ...tail] = [1, 2, 3, 4];
// head = 1, tail = [2, 3, 4]   — rest collects the leftovers

The classic swap — no temp variable needed:

let a = 1, b = 2;
[a, b] = [b, a];
// a = 2, b = 1

Works on anything iterable, not just arrays:

const [r, g, b] = "abc";          // r = "a", g = "b", b = "c"
const [first, second] = new Set([10, 20, 30]);

Object Destructuring

const user = { name: "Ada", age: 37, role: "admin" };

const { name, age } = user;
// name = "Ada", age = 37

// Rename
const { name: userName, role: userRole } = user;
// userName = "Ada", userRole = "admin"

// Default
const { country = "Unknown" } = user;
// country = "Unknown"  — `country` was missing

// Rename + default
const { country: c = "n/a" } = user;
// c = "n/a"

// Rest — collect remaining properties
const { name: n, ...rest } = user;
// n = "Ada", rest = { age: 37, role: "admin" }

Order doesn’t matter for objects (it’s by key, not position):

const { age, name } = user;   // same result as { name, age }

Nested Destructuring

You can destructure deep into structure in one line:

const response = {
  data: {
    user: { name: "Ada", address: { city: "Mumbai" } },
  },
  status: 200,
};

const { data: { user: { name, address: { city } } }, status } = response;
// name = "Ada", city = "Mumbai", status = 200

Two cautions with deep destructuring:

  1. Readability falls off a cliff beyond two levels.
  2. Any missing intermediate throws:
const { a: { b } } = {};   // ❌ TypeError — `a` is undefined, can't read .b

A safer form when intermediates might be missing:

const { a: { b } = {} } = {};
// b = undefined  — default for the missing `a`

Or just use optional chaining instead:

const b = obj?.a?.b;

Destructuring in Function Parameters

The single biggest use case — “named arguments”:

function createUser({ name, age, role = "user" } = {}) {
  return { name, age, role };
}

createUser({ name: "Ada", age: 37 });
// { name: "Ada", age: 37, role: "user" }

createUser();   // works thanks to `= {}` default

The trailing = {} is critical. Without it, calling createUser() with no argument throws because you can’t destructure undefined.

Arrays in parameters work the same way:

function add([a, b]) {
  return a + b;
}
add([1, 2]);   // 3

Destructuring Return Values

A function returning multiple things via an object:

function parseFullName(name) {
  const [first, ...rest] = name.split(" ");
  return { first, last: rest.join(" ") };
}

const { first, last } = parseFullName("Ada Augusta Lovelace");
// first = "Ada", last = "Augusta Lovelace"

Or via a tuple-like array:

function minMax(arr) {
  return [Math.min(...arr), Math.max(...arr)];
}

const [low, high] = minMax([5, 3, 9, 1]);
// low = 1, high = 9

Iterating an Object With Destructuring

Object.entries + destructured for…of is the readable form:

const scores = { Ada: 95, Lin: 88, Rae: 91 };

for (const [name, score] of Object.entries(scores)) {
  console.log(`${name}: ${score}`);
}
// Output:
// Ada: 95
// Lin: 88
// Rae: 91

Same trick in map/filter:

const passed = Object.entries(scores)
  .filter(([, score]) => score >= 90)
  .map(([name]) => name);
// ["Ada", "Rae"]

Note the [, score] — skipping the unused name with a leading comma.

Destructured Imports

A common destructuring case you may not have recognised:

import { readFile, writeFile } from "node:fs/promises";
const { useState, useEffect } = React;

Mechanically the same as object destructuring.

Rename and Default Together

The most expressive form combines rename + default + nested:

function configure({
  host:   serverHost = "localhost",
  port:   serverPort = 8080,
  tls: { enabled: tlsEnabled = false, cert } = {},
} = {}) {
  console.log({ serverHost, serverPort, tlsEnabled, cert });
}

configure();
// { serverHost: "localhost", serverPort: 8080, tlsEnabled: false, cert: undefined }

configure({ host: "api.example.com", tls: { enabled: true, cert: "abc.pem" } });
// { serverHost: "api.example.com", serverPort: 8080, tlsEnabled: true, cert: "abc.pem" }

Destructuring vs Accessing — When to Use Which

Situation Use
Pulling 2+ properties out of one object destructure
Single property, used once direct access (obj.x)
Function with 3+ params, especially with defaults destructure parameters
Iterating array of [key, value] pairs destructure
Dynamic property name bracket access (obj[key])
Possibly-missing intermediates optional chaining (obj?.a?.b)

Tip: Don’t over-destructure. If you only need user.name once, write user.name. Destructuring shines when you’re using multiple properties or aliasing them.

A Realistic Example — HTTP Handler

async function handleRequest(req, res) {
  // Destructure incoming
  const {
    method,
    url,
    headers: { "x-request-id": requestId = crypto.randomUUID() },
  } = req;

  // Destructure parsed query
  const { page = 1, limit = 20, q = "" } = parseQuery(url);

  // Pretend this calls a service that returns { items, total }
  const { items, total } = await db.search({ q, offset: (page - 1) * limit, limit });

  res.json({ requestId, method, page, total, items });
}

Three nested objects flattened into one block of named locals. Compare with the manual form:

// Without destructuring — more typing, more places for typos
const method = req.method;
const url = req.url;
const requestId = (req.headers && req.headers["x-request-id"]) || crypto.randomUUID();
const query = parseQuery(url);
const page = query.page === undefined ? 1 : query.page;
const limit = query.limit === undefined ? 20 : query.limit;
const q = query.q === undefined ? "" : query.q;
// ...

Common Mistakes

// ❌ No default for the destructured object
function fn({ x }) {}
fn();    // TypeError — cannot destructure undefined

// ✅
function fn({ x } = {}) {}

// ❌ Destructuring null
const { x } = null;     // TypeError

// ❌ Variable name vs key — easy typo
const { naame } = { name: "Ada" };
// naame = undefined  (silent!)

// ❌ Trying to destructure before declaring
{
  let { x } = obj;
  let x = 1;      // SyntaxError — already declared via destructuring
}

// ❌ Re-using a name across assignment in array destructuring
let a, b;
[a, b] = obj;     // works
let [a, b] = obj; // ❌ a already declared

// ✅ Use { } syntax once

Best Practices Checklist

  • ✅ Destructure function parameters with defaults for cleaner APIs
  • ✅ Always default the destructured object: ({...} = {})
  • ✅ Use rename to give better local names: ({ id: userId })
  • ✅ Combine with optional chaining when intermediates can be missing
  • ✅ Use destructuring in for…of Object.entries(obj) for clean iteration
  • ❌ Don’t go more than two levels deep — extract intermediate steps
  • ❌ Don’t destructure a single property that’s used once
  • ❌ Don’t forget the = {} default — it’s the most common bug

What’s Next?

You can pull data out of structures concisely. The inverse is the spread operator (...), which puts pieces back together. Plus the matching rest parameter you’ve already met. Next chapter covers both in detail.

← Previous: JS Hoisting Next: JS Spread & Rest →

Updated: