Skip to main content

šŸ“¦ Destructuring & Spread Operator

Data in JavaScript lives in arrays and objects — but reading and reshaping it used to take a lot of ceremony. ES6 gave us two syntaxes that changed everyday code: destructuring, which unpacks values out of a structure in one clean line, and the spread/rest ..., which copies, merges, and collects them just as easily.

Week 3 · Day 1 (Monday: ES6+ Features) · Lecture 2

šŸŽÆ Learning Objectives

By the end of this lesson, you will be able to:

  • Destructure arrays and objects, including renaming, defaults, and nesting
  • Destructure function parameters to write cleaner, self-documenting APIs
  • Use the spread operator to copy and merge arrays and objects
  • Distinguish the rest use of ... (collecting) from the spread use (expanding)
  • Write immutable update patterns the way React state demands
  • Recognize the shallow-copy trap and know when you need a deep clone

Estimated Time: 60 minutes

Practice: Reshape an API response and build immutable shopping-cart operations.

In This Lesson

Unpacking Your Data

Picture a delivery box with several items inside. Without destructuring you lift each item out one at a time and set it down somewhere. Destructuring lets you unpack the whole box in a single move, naming each item as it comes out. The spread operator is the opposite motion — it tips a box out into a bigger one, letting you copy and combine structures effortlessly.

Both are pure syntax sugar over things you could already do the long way — but they make code so much shorter and clearer that they've become the default style across React, Node, and the whole ecosystem.

Array Destructuring

Array destructuring pulls values out by position. The variable names on the left line up with the elements on the right.

Array elements mapped by position into three named variables 'red' 'green' 'blue' colors[ ] first = 'red' second = 'green' third = 'blue'
Array destructuring binds by position: the first name gets element 0, and so on.
const colors = ['red', 'green', 'blue'];

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

// Skip elements with an empty slot
const [primary, , tertiary] = colors;
console.log(primary, tertiary);      // 'red' 'blue'

// Default values fill in when the source is short
const [a, b, c, d = 'yellow'] = colors;
console.log(d);                      // 'yellow'

// Collect the leftovers with a rest element
const [head, ...tail] = colors;
console.log(head);                   // 'red'
console.log(tail);                   // ['green', 'blue']

// A famously clean trick: swap two variables with no temp
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y);                   // 2 1

Object Destructuring

Object destructuring pulls values out by property name, not position — so the order inside the braces doesn't matter, only the names.

const user = {
    name: 'Alice',
    age: 30,
    email: 'alice@example.com',
    address: { city: 'New York', country: 'USA' }
};

// Grab properties by name
const { name, age, email } = user;

// Rename as you extract — property : newVariableName
const { name: userName, age: userAge } = user;
console.log(userName, userAge);      // 'Alice' 30

// Default value for a property that may be missing
const { phone = 'N/A' } = user;
console.log(phone);                  // 'N/A'

// Reach into nested objects
const { address: { city, country } } = user;
console.log(city, country);          // 'New York' 'USA'

// Rest gathers every remaining property into a new object
const { name: n, ...rest } = user;
console.log(rest);                   // { age: 30, email: '...', address: {...} }

šŸ’” Nested destructuring only reads

const { address: { city } } = user; creates city but not address — the address: part is a path, not a new binding. If you want both, list them separately.

Destructuring Parameters

One of the highest-value uses is destructuring right in a function's parameter list. The signature becomes self-documenting — a reader sees exactly which fields the function needs — and defaults handle optional values.

// Instead of reaching into `user` inside the body...
function createUser({ name, email, role = 'user' }) {
    console.log(`Creating ${name} (${email}) as ${role}`);
}
createUser({ name: 'Bob', email: 'bob@example.com' });
// "Creating Bob (bob@example.com) as user"

// Destructure a fetched response inline
async function loadUser() {
    const res = await fetch('/api/user');
    const { data: { user, preferences }, status } = await res.json();
    console.log(user, preferences, status);
}

// This is exactly how React components read their props:
function UserProfile({ user, onEdit, onDelete }) {
    // `user`, `onEdit`, `onDelete` are ready to use — no props.xyz
}

āš ļø Guard against undefined

Destructuring undefined throws. If a function might be called with no argument, give the whole parameter a default: function f({ a } = {}) { ... }. The = {} means "if nothing was passed, destructure an empty object instead."

Spread with Arrays

The spread operator (...) expands an iterable into its individual elements wherever a list of values is expected. Think of spreading butter — it takes something packed and lays it out flat.

// Copy an array (a fresh array, not a shared reference)
const original = [1, 2, 3];
const copy = [...original];

// Concatenate
const combined = [...[1, 2, 3], ...[4, 5, 6]];   // [1,2,3,4,5,6]

// Insert elements around a spread
const numbers = [2, 3, 4];
const more = [1, ...numbers, 5];                 // [1,2,3,4,5]

// Turn any iterable into an array
const chars = [...'hello'];                       // ['h','e','l','l','o']

// Pass array items as separate arguments
console.log(Math.max(...[4, 1, 9, 3]));           // 9
graph LR A["arr1 = [1,2,3]"] --> C["[...arr1, ...arr2]"] B["arr2 = [4,5,6]"] --> C C --> D["[1,2,3,4,5,6]"]

Spread with Objects

Object spread copies an object's own enumerable properties into a new object. When keys collide, the last one wins — which is exactly what makes it perfect for applying overrides on top of defaults.

// Copy
const copied = { ...{ name: 'Alice', age: 30 } };

// Merge — later sources override earlier ones
const defaults  = { theme: 'light', notifications: true };
const userPrefs = { theme: 'dark' };
const settings  = { ...defaults, ...userPrefs };
console.log(settings);   // { theme: 'dark', notifications: true }

// Add or override a property while copying
const user = { name: 'Bob', age: 25 };
const updated = { ...user, age: 26, email: 'bob@example.com' };
console.log(updated);    // { name: 'Bob', age: 26, email: 'bob@example.com' }

āš ļø Spread is a SHALLOW copy

const nested = { user: { name: 'Alice' } };
const shallow = { ...nested };
shallow.user.name = 'Bob';
console.log(nested.user.name);   // 'Bob' — the inner object is still shared!

Top-level properties are copied, but nested objects are shared by reference. To copy the inner objects too, spread them at each level, or use structuredClone(nested) (built into modern browsers and Node 17+) for a true deep clone.

Rest vs Spread — same dots, opposite jobs

The ... token means two different things depending on where it sits. On the left of an = or in a parameter list it's rest — it collects many values into one array or object. On the right, inside a call or literal, it's spread — it expands one structure into many values.

graph TD A["Rest ... (collecting)"] --> B[Many values → one array/object] C["Spread ... (expanding)"] --> D[One array/object → many values]
// REST: gather trailing arguments into an array
function multiply(factor, ...numbers) {
    return numbers.map(n => n * factor);
}
console.log(multiply(2, 1, 2, 3, 4));   // [2, 4, 6, 8]

// SPREAD: expand an array into those same arguments
const arr = [1, 2, 3, 4];
console.log(multiply(2, ...arr));       // [2, 4, 6, 8]

// Rest also works in destructuring (as we saw earlier)
const [firstItem, ...others] = [1, 2, 3, 4, 5];
console.log(others);                    // [2, 3, 4, 5]

Immutable Update Patterns

Modern front-end frameworks — React above all — expect you to treat state as immutable: never mutate the existing object, always produce a new one. Spread is the tool that makes this ergonomic. This is the single most common place you'll use it professionally.

// A Redux-style reducer builds a new state object each time
function userReducer(state = {}, action) {
    switch (action.type) {
        case 'UPDATE_USER':
            return { ...state, ...action.payload };
        case 'UPDATE_PROFILE':
            return {
                ...state,
                profile: { ...state.profile, ...action.payload }
            };
        default:
            return state;
    }
}

// The React useState functional-update pattern
setUser(prev => ({ ...prev, email: 'new@email.com' }));

// Immutable array helpers — none of these mutate the input
const addItem    = (arr, item)        => [...arr, item];
const removeItem = (arr, i)           => [...arr.slice(0, i), ...arr.slice(i + 1)];
const updateItem = (arr, i, newItem)  => [...arr.slice(0, i), newItem, ...arr.slice(i + 1)];

āš ļø Don't spread inside a loop

// āŒ O(n²): rebuilds the whole array every iteration
let result = [];
for (const item of items) result = [...result, process(item)];

// āœ… O(n): map, or push into one array
const result = items.map(process);

Practice & Quiz

šŸ‹ļø Exercise 1: Reshape an API response

Goal: The API returns snake_case keys and a nested preferences object. Use destructuring (with renaming and nesting) and spread to produce a flat, camelCase object.

const apiResponse = {
    user_id: 123,
    user_name: 'johndoe',
    user_email: 'john@example.com',
    created_at: '2023-01-01',
    preferences: { theme: 'dark', notifications: true }
};
// Target:
// { id: 123, name: 'johndoe', email: 'john@example.com',
//   createdAt: '2023-01-01', theme: 'dark', notifications: true }
function transformUserData(data) {
    // your code
}
šŸ’” Hint

Destructure and rename in one shot: const { user_id: id, ... , preferences } = data;. Then destructure preferences and return an object that spreads nothing — just lists the renamed values plus ... the prefs.

āœ… Solution
function transformUserData(data) {
    const {
        user_id: id,
        user_name: name,
        user_email: email,
        created_at: createdAt,
        preferences
    } = data;
    return { id, name, email, createdAt, ...preferences };
}
console.log(transformUserData(apiResponse));

šŸ‹ļø Exercise 2: Immutable cart update

Goal: Write updateQuantity(cart, itemId, quantity) that returns a new cart with the matching item's quantity changed — without mutating the original.

const cart = {
    items: [
        { id: 1, name: 'Book', price: 20, quantity: 1 },
        { id: 2, name: 'Pen',  price: 5,  quantity: 2 }
    ]
};
šŸ’” Hint

map over cart.items; for the matching id return { ...item, quantity }, otherwise return item unchanged. Then return { ...cart, items: newItems }.

āœ… Solution
function updateQuantity(cart, itemId, quantity) {
    return {
        ...cart,
        items: cart.items.map(item =>
            item.id === itemId ? { ...item, quantity } : item
        )
    };
}
const next = updateQuantity(cart, 1, 3);
console.log(next.items[0].quantity);   // 3
console.log(cart.items[0].quantity);   // 1 — original untouched

šŸŽÆ Quick Quiz

Question 1: What is tail after const [head, ...tail] = [1, 2, 3];?

Question 2: What does { ...{a:1, b:2}, b:9 } evaluate to?

Question 3: After const shallow = { ...obj }, you change a property on a nested object inside shallow. What happens to obj?

Best Practices & Pitfalls

āœ… Do

  • Destructure function parameters to make signatures self-documenting
  • Provide default values for properties that may be absent
  • Use spread for immutable updates in React/Redux state
  • Guard against destructuring undefined with = {} on the parameter

āŒ Don't

  • Assume spread deep-copies — it's shallow; nested objects stay shared
  • Spread inside a loop to build up an array (it's O(n²))
  • Over-nest destructuring until it becomes unreadable — split it into steps
  • Forget that array destructuring is positional, so order matters

āœ… Deep clone when you truly need one

// Modern, built-in, handles nesting (Node 17+, all current browsers):
const deep = structuredClone(original);

// Older fallback — loses functions, Dates become strings, etc.:
const deepJson = JSON.parse(JSON.stringify(original));

Summary

šŸŽ‰ Key Takeaways

  • Array destructuring binds by position; object destructuring binds by name
  • You can rename, supply defaults, and reach into nested structures
  • Destructuring parameters (with = {} as a guard) makes function APIs cleaner
  • Spread expands and copies; keys that collide are won by the last source
  • Same dots, opposite jobs: rest collects, spread expands
  • Spread is a shallow copy — use structuredClone for deep clones
  • These patterns are the backbone of immutable state updates

šŸ“š Additional Resources

šŸš€ What's Next?

You can now reshape data with ease. The next lesson brings structure to your code itself: Classes and Modules — organizing behavior into reusable, encapsulated units and splitting your app across files with import/export.

šŸŽ‰ Well done!

Destructuring and spread will appear in almost every component and reducer you write.