Destructuring and Spread/Rest Operators in JavaScript

Destructuring and Spread/Rest Operators in JavaScript

Destructuring and Spread/Rest Operators in JavaScript

I still remember the exact moment I fell in love with destructuring. I was refactoring a function that took an options object, and instead of writing options.width, options.height, options.color five times, I wrote one line and got all three as clean local variables. That one feature, combined with the spread and rest operators, changed the way I write almost every function I touch today. Let me walk you through all of it — from the basics to the internal mechanics.

What Destructuring Actually Is

Destructuring is a syntax that lets me unpack values from arrays or properties from objects into distinct variables, in a single, readable expression. It doesn’t do anything I couldn’t do manually before — it’s purely a more expressive way to extract data.

Array Destructuring

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

const [first, second, third] = colors;
console.log(first, second, third); // red green blue

I can skip elements I don’t need:

const [primary, , tertiary] = colors;
console.log(primary, tertiary); // red blue

I can assign default values in case the array doesn’t have enough elements:

const [a, b, c, d = "yellow"] = colors;
console.log(d); // yellow (since colors only has 3 items)

I can swap variables without a temporary variable, which used to require three lines:

let x = 1;
let y = 2;
[x, y] = [y, x];
console.log(x, y); // 2 1

Nested Array Destructuring

const matrix = [[1, 2], [3, 4]];
const [[a1, a2], [b1, b2]] = matrix;
console.log(a1, a2, b1, b2); // 1 2 3 4

Object Destructuring

const user = { name: "Alice", age: 30, city: "Lahore" };

const { name, age } = user;
console.log(name, age); // Alice 30

Renaming Variables

Sometimes the property name in the object clashes with a variable I already have, so I rename it:

const { name: userName, city: userCity } = user;
console.log(userName, userCity); // Alice Lahore

Default Values

const { country = "Pakistan" } = user;
console.log(country); // Pakistan, since "country" doesn't exist on user

Nested Object Destructuring

const employee = {
  name: "Bob",
  address: {
    city: "Karachi",
    zip: "74200",
  },
};

const {
  address: { city, zip },
} = employee;

console.log(city, zip); // Karachi 74200

Note that this doesn’t create a variable called address — only city and zip are extracted. If I want both, I’d write const { address, address: { city, zip } } = employee;.

Destructuring Function Parameters

This is one of my favorite everyday uses — it makes function signatures self-documenting.

function createUser({ name, age = 18, role = "member" }) {
  return `${name} (${age}) - ${role}`;
}

console.log(createUser({ name: "Zain", age: 25 }));
// Zain (25) - member

The Spread Operator (...)

The spread operator expands an iterable (array, string, or object) into individual elements. I use it constantly when copying and merging data.

Spreading Arrays

const nums1 = [1, 2, 3];
const nums2 = [4, 5, 6];

const combined = [...nums1, ...nums2];
console.log(combined); // [1, 2, 3, 4, 5, 6]

const copy = [...nums1];
copy.push(4);
console.log(nums1); // [1, 2, 3] (original untouched)

Spreading into Function Calls

function sum(a, b, c) {
  return a + b + c;
}

const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6

Spreading Objects

const baseConfig = { theme: "dark", fontSize: 14 };
const userConfig = { fontSize: 16, showLineNumbers: true };

const finalConfig = { ...baseConfig, ...userConfig };
console.log(finalConfig);
// { theme: "dark", fontSize: 16, showLineNumbers: true }

Notice that when keys overlap, the later spread wins — fontSize ends up as 16 because userConfig was spread second. I rely on this ordering constantly when merging default and override configs.

Spreading Strings

const letters = [..."hello"];
console.log(letters); // ["h", "e", "l", "l", "o"]

The Rest Operator (...)

The rest operator looks identical to spread but does the opposite job — it collects multiple elements into a single array or object, rather than expanding them.

Rest in Function Parameters

function sumAll(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}

console.log(sumAll(1, 2, 3, 4, 5)); // 15

This replaced the old, clunky arguments object for me — ...numbers is a real array with all array methods available, whereas arguments was only array-like.

Rest in Array Destructuring

const scores = [95, 88, 76, 60, 45];
const [top, ...rest] = scores;
console.log(top);  // 95
console.log(rest); // [88, 76, 60, 45]

Rest in Object Destructuring

const product = { id: 1, name: "Laptop", price: 1200, inStock: true };

const { id, ...details } = product;
console.log(id);      // 1
console.log(details); // { name: "Laptop", price: 1200, inStock: true }

I use this pattern all the time to strip out one property while keeping the rest, like removing a password field before logging a user object.

Spread vs. Rest: The Key Difference

SpreadRest
PurposeExpands elements outCollects elements into one array/object
PositionUsed where a list of values is expectedUsed in function parameters or destructuring patterns
Example[...arr], fn(...args)function f(...args), const [a, ...b] = arr

The syntax is identical — three dots — but the context determines whether it’s spreading or resting. I always look at whether it’s on the “producing” side (spread) or “receiving” side (rest) of an assignment.

Internal Working: How This Actually Works

Destructuring relies on the iterator protocol for arrays and iterables, and on simple property lookup for objects. When I write const [a, b] = someIterable, the engine internally calls someIterable[Symbol.iterator]() and pulls values one at a time using .next(). This is why destructuring works not just on arrays, but on any iterable — strings, Map, Set, generator results, and NodeLists.

const set = new Set([10, 20, 30]);
const [first, second] = set;
console.log(first, second); // 10 20

For objects, destructuring is closer to plain property access — it does not use the iterator protocol, it directly reads named properties (including inherited enumerable properties, unlike Object.keys).

The spread operator for arrays and strings also uses the iterator protocol under the hood, which is why spreading a Map or Set produces meaningful results, while spread on objects uses [[OwnPropertyKeys]] and copies own enumerable properties only — it does not copy properties from the prototype chain, and importantly, object spread creates a shallow copy, not a deep clone.

const original = { nested: { value: 1 } };
const copy = { ...original };
copy.nested.value = 99;
console.log(original.nested.value); // 99 — the nested object is shared by reference!

This shallow-copy behavior trips people up constantly, myself included early on. If I need a true deep clone, I reach for structuredClone() (built into modern browsers and Node.js) instead of spread.

const deepCopy = structuredClone(original);
deepCopy.nested.value = 500;
console.log(original.nested.value); // 99 — unaffected this time

Practical, Real-World Applications

Cloning and Updating State Immutably (React-style)

const state = { user: { name: "Alice" }, loading: false, error: null };

const newState = {
  ...state,
  loading: true,
};

This pattern is the backbone of immutable state updates in frameworks like React and Redux.

Merging Configuration with Defaults

function initChart(userOptions = {}) {
  const defaults = { width: 400, height: 300, color: "blue" };
  const options = { ...defaults, ...userOptions };
  return options;
}

console.log(initChart({ color: "red" }));
// { width: 400, height: 300, color: "red" }

Extracting Specific Fields from an API Response

async function getUserSummary(userId) {
  const response = await fetch(`/api/users/${userId}`);
  const { name, email, ...rest } = await response.json();
  console.log("Extra fields not needed:", rest);
  return { name, email };
}

Converting NodeLists with Spread

const items = [...document.querySelectorAll(".item")];
items.forEach((item) => console.log(item.textContent));

querySelectorAll returns a NodeList, which is iterable but doesn’t have all Array methods like .map() — spreading it into a real array gives me full array method access.

Best Practices

Common Mistakes to Avoid

  1. Assuming spread creates a deep copy — it doesn’t; nested objects/arrays are still shared by reference.
  2. Destructuring undefined or null — const { a } = undefined throws a TypeError. I always ensure the value exists, or provide a default: const { a } = obj ?? {}.
  3. Forgetting rest must come last — const [...rest, last] = arr is a SyntaxError; rest elements must be the final item in the pattern.
  4. Overusing deeply nested destructuring in function signatures, which can hurt readability.

Debugging Tips

FAQs

Q: Does destructuring mutate the original array or object? A: No, destructuring only reads values; it doesn’t modify the source.

Q: Can I use default values and renaming together in object destructuring? A: Yes: const { name: userName = "Guest" } = obj;

Q: Is spread the same as Object.assign()? A: They’re very similar for shallow copying and merging objects, but spread is more concise and works with array literals too, while Object.assign() mutates its first (target) argument unless you pass an empty object as the target.

Q: Can rest parameters be combined with regular parameters? A: Yes, as long as the rest parameter is last: function f(a, b, ...rest) {}.

Summary and Key Takeaways

Once these patterns become second nature, you’ll notice your code getting shorter, clearer, and far less error-prone, especially around state management and API data handling.

References

Exit mobile version