JavaScriptbeginner

Object & Array Destructuring + Spread

Master JavaScript destructuring, spread, and rest patterns. Learn object and array destructuring, nested patterns, default values, renaming, and shallow copy gotchas.

15 min readยทPublished Mar 6, 2026
destructuringspreadrestjavascript

What Is Destructuring?

Destructuring is a syntax that lets you unpack values from arrays or properties from objects into distinct variables. Instead of accessing properties one at a time, you extract what you need in a single statement.

// Without destructuring
const user = { name: 'Alice', age: 30, role: 'admin' };
const name = user.name;
const age = user.age;
const role = user.role;

// With destructuring
const { name, age, role } = user;

Same result, one line instead of three. But destructuring goes much deeper than simple extraction โ€” it handles nested objects, arrays, defaults, renaming, and rest patterns.

Object Destructuring

Basic Syntax

Extract properties by name. The variable names must match the property names.

const config = {
  host: 'localhost',
  port: 3000,
  debug: true,
};

const { host, port, debug } = config;

console.log(host);  // 'localhost'
console.log(port);  // 3000
console.log(debug); // true

Order doesn't matter โ€” it's matched by name, not position:

const { debug, host, port } = config; // same result

Default Values

If a property is undefined, a default kicks in:

const options = { color: 'blue' };

const { color, size = 'medium', visible = true } = options;

console.log(color);   // 'blue' โ€” from object
console.log(size);    // 'medium' โ€” default
console.log(visible); // true โ€” default

Defaults only apply to undefined, not null:

const { a = 10, b = 20 } = { a: null, b: undefined };

console.log(a); // null โ€” not undefined, so default doesn't apply
console.log(b); // 20 โ€” undefined triggers the default

Renaming (Aliasing)

Use : to assign to a different variable name:

const apiResponse = {
  user_name: 'Alice',
  user_age: 30,
  is_active: true,
};

const {
  user_name: userName,
  user_age: userAge,
  is_active: isActive,
} = apiResponse;

console.log(userName); // 'Alice'
console.log(userAge);  // 30
console.log(isActive); // true
// console.log(user_name); // ReferenceError โ€” original name not created

Renaming + Defaults

You can combine renaming and defaults:

const data = { old_name: 'Alice' };

const {
  old_name: name = 'Unknown',
  old_email: email = 'no-email',
} = data;

console.log(name);  // 'Alice' โ€” renamed from old_name
console.log(email); // 'no-email' โ€” not in object, uses default

Read it as: "take old_name, call it name, default to 'Unknown'."

Nested Object Destructuring

Dig into nested objects:

const response = {
  data: {
    user: {
      id: 1,
      profile: {
        firstName: 'Alice',
        lastName: 'Smith',
        address: {
          city: 'Portland',
          state: 'OR',
        },
      },
    },
  },
  status: 200,
};

const {
  data: {
    user: {
      id,
      profile: {
        firstName,
        lastName,
        address: { city, state },
      },
    },
  },
  status,
} = response;

console.log(firstName); // 'Alice'
console.log(city);      // 'Portland'
console.log(status);    // 200
// console.log(data);   // ReferenceError โ€” 'data' is not a variable
// console.log(profile); // ReferenceError โ€” intermediate names aren't bound

Important: intermediate names (data, user, profile, address) are not created as variables. Only the leaf names are. If you need both the nested value and the parent object, destructure separately:

const { data } = response;
const { user } = data;
const { profile: { firstName } } = user;

Computed Property Names

Use bracket notation to destructure dynamic keys:

const key = 'name';
const obj = { name: 'Alice', age: 30 };

const { [key]: value } = obj;
console.log(value); // 'Alice'

// Useful in functions
function getProperty(obj, prop) {
  const { [prop]: value } = obj;
  return value;
}

getProperty({ x: 10, y: 20 }, 'x'); // 10

Array Destructuring

Basic Syntax

Array destructuring works by position, not by name:

const colors = ['red', 'green', 'blue'];

const [first, second, third] = colors;

console.log(first);  // 'red'
console.log(second); // 'green'
console.log(third);  // 'blue'

Skipping Elements

Use commas to skip values:

const [, , blue] = ['red', 'green', 'blue'];
console.log(blue); // 'blue'

const [first, , third] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(third); // 3

Default Values

Same as objects โ€” defaults apply when the value is undefined:

const [a = 10, b = 20, c = 30] = [1, 2];

console.log(a); // 1
console.log(b); // 2
console.log(c); // 30 โ€” no third element, uses default

Swap Variables

The classic trick โ€” no temp variable needed:

let x = 1;
let y = 2;

[x, y] = [y, x];

console.log(x); // 2
console.log(y); // 1

// Works with more than two
let a = 1, b = 2, c = 3;
[a, b, c] = [c, a, b];
// a = 3, b = 1, c = 2

Nested Array Destructuring

const matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
];

const [[a, , c], , [g]] = matrix;

console.log(a); // 1
console.log(c); // 3
console.log(g); // 7

Destructuring from Functions

Very common pattern โ€” functions return arrays or objects, destructuring extracts the values:

// Array return (like React hooks)
function useState(initial) {
  let value = initial;
  const setter = (newVal) => { value = newVal; };
  return [value, setter];
}

const [count, setCount] = useState(0);

// Object return (like API responses)
function fetchUser() {
  return { data: { name: 'Alice' }, error: null, loading: false };
}

const { data, error, loading } = fetchUser();

Rest Patterns

Rest in Objects (...rest)

Collect remaining properties into a new object:

const user = { name: 'Alice', age: 30, role: 'admin', email: '[email protected]' };

const { name, ...rest } = user;

console.log(name); // 'Alice'
console.log(rest); // { age: 30, role: 'admin', email: '[email protected]' }

This is great for removing a property without mutating the original:

// Remove 'password' from user object
const userWithPassword = { id: 1, name: 'Alice', password: 'secret123' };

const { password, ...safeUser } = userWithPassword;

console.log(safeUser); // { id: 1, name: 'Alice' } โ€” no password
// userWithPassword still has password โ€” not mutated

Rest in Arrays

Collect remaining elements into a new array:

const [first, second, ...remaining] = [1, 2, 3, 4, 5];

console.log(first);     // 1
console.log(second);    // 2
console.log(remaining); // [3, 4, 5]

Rest must be the last element:

// SyntaxError: Rest element must be last element
const [...rest, last] = [1, 2, 3];

Rest in Function Parameters

function logFirst(first, ...others) {
  console.log('First:', first);
  console.log('Others:', others);
}

logFirst('a', 'b', 'c', 'd');
// First: a
// Others: ['b', 'c', 'd']

// Combine with destructuring
function createUser({ name, email, ...metadata }) {
  return {
    name,
    email,
    metadata, // everything else
    createdAt: Date.now(),
  };
}

createUser({ name: 'Alice', email: '[email protected]', role: 'admin', dept: 'eng' });
// {
//   name: 'Alice',
//   email: '[email protected]',
//   metadata: { role: 'admin', dept: 'eng' },
//   createdAt: ...
// }

Spread Operator

Spread (...) looks like rest but does the opposite โ€” it expands an iterable into individual elements.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Context      โ”‚ Behavior                                  โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Rest (left)  โ”‚ const { a, ...rest } = obj  โ† collects  โ”‚
โ”‚ Spread (right)โ”‚ const copy = { ...obj }    โ† expands    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Spread in Arrays

const a = [1, 2, 3];
const b = [4, 5, 6];

// Combine arrays
const combined = [...a, ...b];
// [1, 2, 3, 4, 5, 6]

// Insert in the middle
const inserted = [0, ...a, 3.5, ...b, 7];
// [0, 1, 2, 3, 3.5, 4, 5, 6, 7]

// Copy an array (shallow)
const copy = [...a];
copy.push(99);
console.log(a);    // [1, 2, 3] โ€” original unchanged
console.log(copy); // [1, 2, 3, 99]

// Convert iterable to array
const chars = [...'hello'];
// ['h', 'e', 'l', 'l', 'o']

const unique = [...new Set([1, 2, 2, 3, 3])];
// [1, 2, 3]

// Spread into function arguments
const nums = [5, 2, 8, 1, 9];
Math.max(...nums); // 9 (same as Math.max(5, 2, 8, 1, 9))

Spread in Objects

const defaults = {
  theme: 'dark',
  fontSize: 14,
  language: 'en',
};

const userPrefs = {
  fontSize: 18,
  language: 'fr',
};

// Merge โ€” later spreads override earlier ones
const config = { ...defaults, ...userPrefs };
// { theme: 'dark', fontSize: 18, language: 'fr' }

// Add/override specific properties
const updated = { ...config, theme: 'light', newProp: true };
// { theme: 'light', fontSize: 18, language: 'fr', newProp: true }

Order matters โ€” last one wins:

const a = { x: 1, y: 2 };
const b = { y: 3, z: 4 };

const result1 = { ...a, ...b }; // { x: 1, y: 3, z: 4 } โ€” b's y wins
const result2 = { ...b, ...a }; // { x: 1, y: 2, z: 4 } โ€” a's y wins

Conditional Spread

Conditionally include properties:

const isAdmin = true;
const includeDebug = false;

const config = {
  name: 'App',
  ...(isAdmin && { adminPanel: true, adminTools: ['users', 'logs'] }),
  ...(includeDebug && { debug: true, verbose: true }),
};
// { name: 'App', adminPanel: true, adminTools: ['users', 'logs'] }
// debug/verbose not included because includeDebug is false

Warning: if the condition is false, false && { ... } evaluates to false, and ...false spreads nothing (it's a no-op). But 0 && { ... } also evaluates to 0, and ...0 will throw. Use a ternary for safety:

// Safer pattern
const config = {
  name: 'App',
  ...(isAdmin ? { adminPanel: true } : {}),
};

Destructuring in Function Parameters

One of the most common uses โ€” clean up function signatures:

// Without destructuring โ€” what does each parameter mean?
function createUser(name, email, age, role, active) {
  // ...
}
createUser('Alice', '[email protected]', 30, 'admin', true);

// With destructuring โ€” self-documenting
function createUser({ name, email, age = 0, role = 'user', active = true }) {
  return { name, email, age, role, active, createdAt: Date.now() };
}

createUser({ name: 'Alice', email: '[email protected]', role: 'admin' });
// age defaults to 0, active defaults to true

Optional Parameter Object

Make the entire options object optional:

function connect({ host = 'localhost', port = 3000, ssl = false } = {}) {
  console.log(`Connecting to ${host}:${port} (SSL: ${ssl})`);
}

connect();                    // localhost:3000 (SSL: false)
connect({ port: 8080 });     // localhost:8080 (SSL: false)
connect({ ssl: true });      // localhost:3000 (SSL: true)

The = {} at the end means "if no argument is passed, use an empty object" so the destructuring doesn't throw on undefined.

Mixed Destructuring in Parameters

function processOrder(
  orderId,
  { customer: { name, email }, items, shipping: { method = 'standard' } = {} }
) {
  console.log(`Order ${orderId} for ${name} (${email})`);
  console.log(`${items.length} items, shipping: ${method}`);
}

processOrder(123, {
  customer: { name: 'Alice', email: '[email protected]' },
  items: ['book', 'pen'],
  shipping: { method: 'express' },
});

Shallow Copy Gotchas

Both spread and rest create shallow copies. Nested objects and arrays are shared references, not independent copies.

const original = {
  name: 'Alice',
  scores: [95, 87, 92],
  address: {
    city: 'Portland',
    state: 'OR',
  },
};

const copy = { ...original };

// Top-level properties are independent
copy.name = 'Bob';
console.log(original.name); // 'Alice' โ€” unchanged

// Nested objects are SHARED references
copy.scores.push(100);
console.log(original.scores); // [95, 87, 92, 100] โ€” MUTATED!

copy.address.city = 'Seattle';
console.log(original.address.city); // 'Seattle' โ€” MUTATED!
Shallow copy:

original โ”€โ”€โ–บ { name: 'Alice', scores: โ”€โ”€โ–บ [95, 87, 92], address: โ”€โ”€โ–บ { city: ... } }
                                 โ–ฒ                             โ–ฒ
copy โ”€โ”€โ”€โ”€โ–บ { name: 'Bob',   scores: โ”€โ”˜              address: โ”€โ”˜                   }

copy.name is a separate string (primitive = copied by value)
copy.scores points to the SAME array (object = copied by reference)
copy.address points to the SAME object

Solutions for Deep Copy

// 1. structuredClone (modern, built-in)
const deepCopy = structuredClone(original);
deepCopy.scores.push(100);
console.log(original.scores.length); // 3 โ€” unchanged

// 2. JSON round-trip (older approach, has limitations)
const deepCopy2 = JSON.parse(JSON.stringify(original));
// Loses: functions, undefined, Date objects, RegExp, Map, Set, etc.

// 3. Manual nested spread (tedious but precise)
const manualCopy = {
  ...original,
  scores: [...original.scores],
  address: { ...original.address },
};
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Method               โ”‚ Notes                                โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ { ...obj }           โ”‚ Shallow only                         โ”‚
โ”‚ Object.assign({},obj)โ”‚ Shallow only (same as spread)        โ”‚
โ”‚ JSON parse/stringify โ”‚ Deep, but loses functions, Dates,    โ”‚
โ”‚                      โ”‚ undefined, Infinity, NaN, etc.       โ”‚
โ”‚ structuredClone()    โ”‚ Deep, handles most types, no funcs   โ”‚
โ”‚ lodash.cloneDeep     โ”‚ Deep, handles edge cases             โ”‚
โ”‚ Manual nested spread โ”‚ Precise control, verbose             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Array Shallow Copy Trap

const matrix = [[1, 2], [3, 4]];
const copy = [...matrix];

copy[0].push(99);
console.log(matrix[0]); // [1, 2, 99] โ€” inner array is shared!

// Fix
const deepMatrix = matrix.map((row) => [...row]);
deepMatrix[0].push(99);
console.log(matrix[0]); // [1, 2] โ€” safe

Real-World Patterns

API Response Handling

async function getUser(id) {
  const response = await fetch(`/api/users/${id}`);
  const { data: user, meta: { requestId } } = await response.json();

  return { user, requestId };
}

const { user, requestId } = await getUser(42);

React Component Props

function Card({ title, children, className = '', onClick, ...rest }) {
  return (
    <div
      className={`card ${className}`}
      onClick={onClick}
      {...rest} // spread remaining props onto the DOM element
    >
      <h2>{title}</h2>
      {children}
    </div>
  );
}

// Usage โ€” any extra props (id, style, data-*) pass through
<Card title="Hello" className="featured" id="main-card" data-testid="card">
  <p>Content</p>
</Card>

Configuration Merging

function createApp(userConfig = {}) {
  const defaultConfig = {
    port: 3000,
    host: 'localhost',
    cors: {
      origin: '*',
      methods: ['GET', 'POST'],
      credentials: false,
    },
    logging: {
      level: 'info',
      format: 'json',
    },
  };

  // Shallow merge won't work for nested objects
  // Need to merge each level explicitly
  const config = {
    ...defaultConfig,
    ...userConfig,
    cors: {
      ...defaultConfig.cors,
      ...userConfig.cors,
    },
    logging: {
      ...defaultConfig.logging,
      ...userConfig.logging,
    },
  };

  return config;
}

const app = createApp({
  port: 8080,
  cors: { credentials: true },
  logging: { level: 'debug' },
});
// {
//   port: 8080,
//   host: 'localhost',
//   cors: { origin: '*', methods: ['GET', 'POST'], credentials: true },
//   logging: { level: 'debug', format: 'json' },
// }

State Updates (Immutable Pattern)

const state = {
  user: { name: 'Alice', preferences: { theme: 'dark' } },
  items: [
    { id: 1, text: 'Learn JS', done: false },
    { id: 2, text: 'Build app', done: false },
  ],
};

// Update nested property immutably
const newState = {
  ...state,
  user: {
    ...state.user,
    preferences: {
      ...state.user.preferences,
      theme: 'light',
    },
  },
};

// Toggle a todo item immutably
const toggledState = {
  ...state,
  items: state.items.map((item) =>
    item.id === 1 ? { ...item, done: !item.done } : item
  ),
};

// Add an item immutably
const addedState = {
  ...state,
  items: [...state.items, { id: 3, text: 'Deploy', done: false }],
};

// Remove an item immutably
const removedState = {
  ...state,
  items: state.items.filter((item) => item.id !== 2),
};

Destructuring in Loops

const entries = [
  ['name', 'Alice'],
  ['age', 30],
  ['role', 'admin'],
];

for (const [key, value] of entries) {
  console.log(`${key}: ${value}`);
}

// With Object.entries
const user = { name: 'Alice', age: 30, role: 'admin' };

for (const [key, value] of Object.entries(user)) {
  console.log(`${key}: ${value}`);
}

// Destructuring in map
const users = [
  { name: 'Alice', age: 30 },
  { name: 'Bob', age: 25 },
];

const descriptions = users.map(({ name, age }) => `${name} is ${age}`);
// ['Alice is 30', 'Bob is 25']

Pattern: Extract + Transform

function formatAddress({ street, city, state, zip, country = 'US' }) {
  return `${street}\n${city}, ${state} ${zip}\n${country}`;
}

const user = {
  name: 'Alice',
  address: {
    street: '123 Main St',
    city: 'Portland',
    state: 'OR',
    zip: '97201',
  },
};

const formatted = formatAddress(user.address);

Common Mistakes

Declaring Without Keyword

When destructuring outside a declaration, wrap in parentheses:

let name, age;

// BAD โ€” JS thinks { starts a block
{ name, age } = { name: 'Alice', age: 30 }; // SyntaxError

// GOOD โ€” parentheses prevent block interpretation
({ name, age } = { name: 'Alice', age: 30 });

Missing Nested Guard

Destructuring throws if a nested path hits undefined or null:

const data = { user: null };

// TypeError: Cannot destructure property 'name' of null
const { user: { name } } = data;

// Fix: provide default for the nested object
const { user: { name } = {} } = data;
console.log(name); // undefined (safe)

// Or use optional chaining instead
const name = data.user?.name;

Confusing Rest with Spread

// REST โ€” collects into variable (left side of =)
const { a, ...rest } = { a: 1, b: 2, c: 3 };

// SPREAD โ€” expands into expression (right side of =)
const merged = { ...rest, d: 4 };

// They look the same (...) but do opposite things
// based on which side of the assignment they're on

Key Takeaways

  1. Object destructuring matches by name โ€” order doesn't matter
  2. Array destructuring matches by position โ€” order matters
  3. Defaults only trigger on undefined โ€” not null, not 0, not ''
  4. Rename with colon โ€” { oldName: newName } creates newName not oldName
  5. Rest must be last โ€” { a, ...rest } works, { ...rest, a } doesn't
  6. Spread creates shallow copies โ€” nested objects are shared references
  7. Use structuredClone for deep copies โ€” or manual nested spread for precision
  8. Destructure function parameters โ€” self-documenting, supports defaults
  9. Conditional spread โ€” ...(condition ? { prop: val } : {}) for safe conditional inclusion
  10. Parentheses for reassignment โ€” ({ a } = obj) when destructuring without let/const

Found this helpful?

Support devsofus โ€” help us keep creating free dev guides.

Related Articles