Home Module 13 Spread & Rest

1. Introduction

The spread operator (...) expands an iterable (array, string, or object) into individual elements. The rest parameter (...) does the opposite — it collects multiple values into a single array. They look identical but serve opposite purposes depending on context.

2. Theory

2.1 Spread with arrays

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

// Merge arrays
const merged = [...a, ...b];        // [1,2,3,4,5,6]
const withGap = [...a, 99, ...b];   // [1,2,3,99,4,5,6]

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

// Prepend / append
const prepended = [0, ...a];        // [0,1,2,3]
const appended  = [...a, 4];        // [1,2,3,4]

// Convert NodeList or Set to array
const nodes  = [...document.querySelectorAll('p')];
const unique = [...new Set([1,2,2,3,3,3])]; // [1,2,3]

2.2 Spread with function calls

// Pass array elements as separate arguments
const nums = [3, 1, 4, 1, 5, 9, 2, 6];

// Old way — apply
const max1 = Math.max.apply(null, nums);

// Spread — much cleaner
const max2 = Math.max(...nums);
const min  = Math.min(...nums);
console.log(max2, min); // 9 1

// Custom function
function add(a, b, c) { return a + b + c; }
const args = [1, 2, 3];
console.log(add(...args)); // 6

// Mix spread with other args
console.log(add(1, ...[ 2, 3 ])); // 6

2.3 Spread with objects

const defaults = { theme: 'light', lang: 'en', fontSize: 14 };
const custom   = { theme: 'dark', fontSize: 18 };

// Merge — right side wins on conflict
const merged = { ...defaults, ...custom };
console.log(merged); // { theme:'dark', lang:'en', fontSize:18 }

// Shallow copy
const copy = { ...defaults };
copy.theme = 'custom';
console.log(defaults.theme); // 'light' — unaffected

// Add/override a property immutably
const user    = { name: 'Alice', age: 28, role: 'user' };
const promoted = { ...user, role: 'admin' };  // new object, user unchanged
const older    = { ...user, age: user.age + 1 }; // increment without mutation

2.4 Rest parameters in functions

// Rest — collect extra arguments into an array
function sum(...numbers) {
  return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(sum(1, 2, 3));         // 6
console.log(sum(1, 2, 3, 4, 5));   // 15
console.log(sum());                // 0 — empty array

// Mix fixed params with rest
function log(level, ...messages) {
  console.log(`[${level}]`, ...messages);
}
log('INFO', 'Server started', 'Port 3000'); // [INFO] Server started Port 3000
log('ERROR', 'Connection failed');           // [ERROR] Connection failed

// Rest must be the LAST parameter
function bad(a, ...rest, b) { } // SyntaxError!

2.5 Rest in destructuring

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

// Object rest
const { name, age, ...extras } = { name: 'Alice', age: 28, role: 'admin', dept: 'Eng' };
console.log(name);   // 'Alice'
console.log(age);    // 28
console.log(extras); // { role: 'admin', dept: 'Eng' }

// Omit a property using object rest
function omit(obj, ...keysToOmit) {
  return Object.fromEntries(
    Object.entries(obj).filter(([key]) => !keysToOmit.includes(key))
  );
}
const safe = omit({ name: 'Alice', password: 'secret', age: 28 }, 'password');
console.log(safe); // { name: 'Alice', age: 28 }

2.6 Spread strings

// Spread a string into characters
const chars = [..."hello"];
console.log(chars); // ['h','e','l','l','o']

// Count unique characters
const unique = new Set([..."mississippi"]);
console.log([...unique]); // ['m','i','s','p']
console.log(unique.size); // 4

2.7 Arguments vs rest parameters

// arguments — old object available in regular functions (not arrow)
function oldWay() {
  console.log(arguments[0]); // works but awkward
  // arguments is array-like, not a real array — no .map etc
}

// rest — proper array, works in arrow functions too
const newWay = (...args) => {
  console.log(args[0]); // args is a real array
  return args.map(n => n * 2);
};

console.log(newWay(1, 2, 3)); // [2, 4, 6]

3. Real World Example

// Immutable state updates — pattern used in React and Redux
let state = {
  user: { name: 'Alice', loggedIn: false },
  cart: { items: [], total: 0 },
  ui:   { theme: 'light', sidebarOpen: false }
};

// Update nested state without mutation
function login(state, userName) {
  return {
    ...state,              // spread all top-level keys
    user: {
      ...state.user,       // spread user object
      name:     userName,
      loggedIn: true,
      loginTime: Date.now()
    }
  };
}

function addToCart(state, item) {
  const items = [...state.cart.items, item];
  const total = items.reduce((sum, i) => sum + i.price, 0);
  return {
    ...state,
    cart: { ...state.cart, items, total }
  };
}

state = login(state, 'Alice');
state = addToCart(state, { id: 1, name: 'Shirt', price: 29 });
state = addToCart(state, { id: 2, name: 'Jeans', price: 59 });

console.log(state.user.loggedIn);  // true
console.log(state.cart.items.length); // 2
console.log(state.cart.total);     // 88

4. Code Example

<script>
  // 1. Variadic function — accepts any number of args
  const average = (...nums) => {
    if (!nums.length) return 0;
    return nums.reduce((sum, n) => sum + n, 0) / nums.length;
  };
  console.log(average(10, 20, 30));        // 20
  console.log(average(5, 15, 25, 35, 45)); // 25

  // 2. Merge configs (right side wins)
  const baseConfig = { debug: false, timeout: 5000, retries: 3 };
  function createConfig(overrides = {}) {
    return { ...baseConfig, ...overrides };
  }
  const dev  = createConfig({ debug: true });
  const prod = createConfig({ timeout: 10000, retries: 5 });
  console.log(dev.debug);      // true
  console.log(dev.timeout);    // 5000 (from base)
  console.log(prod.retries);   // 5 (overridden)

  // 3. Array utilities without mutation
  function withoutIndex(arr, index) {
    return [...arr.slice(0, index), ...arr.slice(index + 1)];
  }
  function insertAt(arr, index, ...items) {
    return [...arr.slice(0, index), ...items, ...arr.slice(index)];
  }
  const fruits = ['apple', 'banana', 'cherry'];
  console.log(withoutIndex(fruits, 1));          // ['apple','cherry']
  console.log(insertAt(fruits, 1, 'mango'));     // ['apple','mango','banana','cherry']
  console.log(fruits); // ['apple','banana','cherry'] — unchanged

  // 4. Remove duplicates
  const numbers = [1, 2, 2, 3, 3, 3, 4];
  const unique  = [...new Set(numbers)];
  console.log(unique); // [1, 2, 3, 4]
</script>

5. Code Breakdown

Variadic average function

The rest parameter ...nums collects all arguments into a real array. The guard if (!nums.length) return 0 prevents dividing by zero when called with no arguments. reduce then sums and divides.

createConfig with spread merge

Spreading baseConfig first, then overrides, means override keys replace base keys. The function creates a brand-new object each time — baseConfig is never mutated.

withoutIndex and insertAt

Both functions combine slice and spread to produce new arrays. withoutIndex slices before and after the target index. insertAt uses rest to accept any number of items to insert.

Set + spread for deduplication

new Set(numbers) automatically removes duplicates. [...set] spreads the Set back into an array. This one-liner is the idiomatic way to deduplicate arrays of primitives.

6. Common Mistakes

Mistake 1 — Spread does a shallow copy only

const a = { x: 1, nested: { y: 2 } };
const b = { ...a };
b.nested.y = 99; // modifies a.nested too!
console.log(a.nested.y); // 99 — both share the same nested object

// Fix — deep clone with structuredClone
const c = structuredClone(a);
c.nested.y = 99;
console.log(a.nested.y); // 2 — safe

Mistake 2 — Rest parameter not last

function bad(a, ...rest, b) {} // SyntaxError
// Rest must always be the final parameter

Mistake 3 — Spreading non-iterables into arrays

const num = 42;
console.log([...num]); // TypeError: num is not iterable

// Only spread iterables: arrays, strings, Sets, Maps, NodeLists
console.log([..."hello"]); // ['h','e','l','l','o'] — string is iterable

Mistake 4 — Forgetting that object spread doesn't merge nested objects

const a = { settings: { theme: 'dark', lang: 'en' } };
const b = { settings: { theme: 'light' } };
const merged = { ...a, ...b };
console.log(merged.settings); // { theme: 'light' } — lang is GONE!
// b.settings completely replaced a.settings

// Fix — spread the nested object too
const merged2 = { ...a, settings: { ...a.settings, ...b.settings } };

7. Best Practices

  1. Use spread to copy arrays and objects — never rely on assignment for copies.
  2. Use rest parameters instead of arguments — rest is a real array with all array methods.
  3. Use spread for immutable state updates — create new objects rather than mutating existing ones.
  4. Remember: spread is shallow — nested objects are still shared references.
  5. Use [...new Set(arr)] for the simplest deduplication of primitive arrays.
  6. Be explicit about merge order — in { ...base, ...custom }, custom wins for duplicate keys.

8. Practice Exercise

  1. Write a merge(...objects) function that accepts any number of objects and returns a merged result (later objects win). Use rest and spread.
  2. Use spread to find the max value in an array without a loop: Math.max(...arr). Then test with an empty array and fix it.
  3. Write an immutable updateNested(state, path, value) function. Given state = { a: { b: { c: 1 } } }, update c to 99 without mutating the original.

9. Assignment

Build a "Shopping Cart with Immutable State".

  1. State: { items: [], discount: 0, user: { name: '', loggedIn: false } }.
  2. Write pure functions (no mutation!) for: addItem(state, item), removeItem(state, id), updateQty(state, id, qty), applyDiscount(state, percent), setUser(state, name).
  3. Each function receives state and returns a NEW state object using spread. The original is never modified.
  4. Wire up buttons to call these functions and re-render the cart.
  5. Add a "History" feature: store each state in a stack array using rest/spread. Add undo functionality.

Deliverable: One HTML file.

10. Interview Questions

  1. What is the spread operator?
    The ... syntax that expands an iterable (array, string, Set) into individual elements, or expands an object's properties into another object. Used in array literals, function calls, and object literals.
  2. What is the rest parameter?
    The ... syntax in a function parameter list that collects all remaining arguments into a real array. It must be the last parameter. It replaces the old arguments object and works in arrow functions.
  3. What is the difference between spread and rest?
    Spread expands — it takes one thing and spreads it into many. Rest collects — it takes many things and gathers them into one. Same syntax, opposite direction: spread at the call site, rest at the definition site.
  4. How do you merge two objects with spread?
    const merged = { ...obj1, ...obj2 }. Properties in obj2 override those in obj1 if they share the same key. This is a shallow merge — nested objects are not deeply merged.

11. Additional Resources

  • MDN — Spread syntax
  • MDN — Rest parameters
  • javascript.info — Rest parameters and spread syntax