Beginner0 questionsFull Guide

JavaScript Spread & Rest Operator Interview Questions

The ... operator has two uses: spread (expand) and rest (collect). Learn how each works and the common interview traps.

The Mental Model

Picture two opposite magic tricks with the same wand. The first trick: you hold a handful of marbles and open your hand — they spread out across the table, each marble becoming its own individual item. That's the spread operator. The second trick: you sweep loose marbles on a table into your hand — they collect into one handful. That's the rest operator. Same three dots (...), completely opposite operations. Spread expands one thing into many. Rest collects many things into one. The trick is recognizing which direction is happening: if ... appears on the right side of an assignment or inside a function call, it's spreading out. If it appears on the left side of an assignment or in a function's parameter list, it's collecting in. The key insight: rest and spread are not special syntax — they're the same symbol used in two mirrored situations. Spread says "take this iterable apart and insert each element here." Rest says "take all these remaining elements and pack them into an array/object here." The position in the code tells you which direction the marbles are moving.

The Explanation

Spread with arrays — expand into individual elements

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

// Combine arrays
const combined = [...a, ...b]         // [1, 2, 3, 4, 5, 6]
const prepended = [0, ...a]           // [0, 1, 2, 3]
const appended  = [...a, 4]           // [1, 2, 3, 4]
const inserted  = [...a.slice(0,1), 99, ...a.slice(1)]  // [1, 99, 2, 3]

// Copy an array — shallow
const copy = [...a]        // [1, 2, 3] — new array, same element references
copy.push(4)
a   // [1, 2, 3] — original unchanged

// Spread as function arguments
Math.max(...a)           // 3 — same as Math.max(1, 2, 3)
Math.min(...b)           // 4
console.log(...a)        // 1 2 3 — spread into separate arguments

// Before spread — the old way with apply
Math.max.apply(null, a)  // worked, but spread is cleaner

// Convert other iterables to arrays
const str = 'hello'
[...str]                 // ['h', 'e', 'l', 'l', 'o']

const set = new Set([1, 2, 2, 3])
[...set]                 // [1, 2, 3] — deduplicated

const map = new Map([['a', 1], ['b', 2]])
[...map]                 // [['a', 1], ['b', 2]]

// NodeList to array (browser)
[...document.querySelectorAll('p')]  // real Array with all array methods

Spread with objects — shallow merge and clone

const defaults  = { theme: 'dark', lang: 'en', fontSize: 14, debug: false }
const overrides = { lang: 'fr', debug: true }

// Merge — later properties win
const config = { ...defaults, ...overrides }
// { theme: 'dark', lang: 'fr', fontSize: 14, debug: true }

// The order is significant — rightmost wins on conflicts
const a = { x: 1, y: 2 }
const b = { y: 10, z: 3 }
{ ...a, ...b }   // { x: 1, y: 10, z: 3 } — b's y wins
{ ...b, ...a }   // { y: 2, z: 3, x: 1 }  — a's y wins

// Shallow clone
const user     = { name: 'Alice', age: 30 }
const userCopy = { ...user }
userCopy.name = 'Bob'
user.name    // 'Alice' — own properties are new, but nested objects are still shared

// Add or override specific properties (immutable update pattern)
const updated = { ...user, age: 31 }               // age updated, name preserved
const renamed = { ...user, displayName: user.name } // add new property

// Remove a property (rest to collect the others)
const { password, ...safeUser } = user  // safeUser has everything except password

// Conditional spread — add properties only when condition is true
const extra = isAdmin ? { role: 'admin', permissions: ['read', 'write'] } : {}
const fullUser = { ...user, ...extra }

// Cleaner with short-circuit
const fullUser = {
  ...user,
  ...(isAdmin && { role: 'admin' }),  // adds role only if isAdmin is truthy
}

Rest parameters — collect function arguments

// Rest collects remaining arguments into an array
function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0)
}
sum(1, 2, 3, 4, 5)  // 15
sum(10, 20)          // 30
sum()                // 0

// Rest after fixed parameters — collects only the "rest"
function log(level, ...messages) {
  console.log(`[${level}]`, ...messages)  // spread back out to console.log
}
log('ERROR', 'File not found', 'path: /config')
// [ERROR] File not found path: /config

function first(a, b, ...rest) {
  console.log(a)     // first arg
  console.log(b)     // second arg
  console.log(rest)  // array of remaining
}
first(1, 2, 3, 4, 5)
// 1, 2, [3, 4, 5]

// Rest must be LAST — this is a SyntaxError:
// function bad(first, ...rest, last) {} // SyntaxError

// Rest vs the old arguments object
function oldWay() {
  console.log(arguments)        // array-like, has no .map()
  const arr = Array.from(arguments)  // had to convert
}

function newWay(...args) {
  console.log(args)             // real Array — has .map(), .filter(), etc.
  args.map(n => n * 2)          // works directly
}

Rest in destructuring — collect the remainder

// Array rest — last items
const [first, second, ...rest] = [1, 2, 3, 4, 5]
// first = 1, second = 2, rest = [3, 4, 5]

const [head, ...tail] = [10, 20, 30]
// head = 10, tail = [20, 30]

const [only, ...empty] = [42]
// only = 42, empty = [] — rest is always an array, never undefined

// Object rest — remaining properties
const { name, age, ...everything_else } = { name: 'Alice', age: 30, role: 'admin', active: true }
// everything_else = { role: 'admin', active: true }

// Remove specific properties cleanly
const { password, token, ...publicData } = apiResponse
// publicData has everything except password and token

// Practical: pick/omit utilities
function pick(obj, ...keys) {
  return keys.reduce((acc, key) => ({ ...acc, [key]: obj[key] }), {})
}
function omit(obj, ...keys) {
  const keySet = new Set(keys)
  return Object.fromEntries(Object.entries(obj).filter(([k]) => !keySet.has(k)))
}

pick(user, 'name', 'email')     // { name: 'Alice', email: '...' }
omit(user, 'password', 'token') // everything except password and token

Spread shallow-ness — the critical limitation

// Spread only copies one level deep
const original = {
  name: 'Alice',
  address: {          // nested object
    city: 'Paris',
    zip: '75001'
  },
  scores: [90, 85, 92]  // nested array
}

const copy = { ...original }

// Top-level properties are new copies
copy.name = 'Bob'
original.name  // 'Alice' — ✓ independent

// Nested objects are STILL SHARED references
copy.address.city = 'London'
original.address.city  // 'London' — ✗ mutated the original!

copy.scores.push(100)
original.scores  // [90, 85, 92, 100] — ✗ mutated the original!

// Fix: manually spread nested objects too
const deeperCopy = {
  ...original,
  address: { ...original.address },  // copy nested object
  scores: [...original.scores]       // copy nested array
}

// For truly arbitrary depth: structuredClone(original)
const trueCopy = structuredClone(original)

Spread in practice — the immutable update patterns used everywhere

// React state updates (never mutate, always create new)
const [user, setUser] = useState({ name: 'Alice', age: 30, score: 0 })

// Update one field
setUser(prev => ({ ...prev, age: 31 }))

// Update nested field — spread each level
setUser(prev => ({
  ...prev,
  address: { ...prev.address, city: 'London' }
}))

// Add to array in state
const [items, setItems] = useState([])
setItems(prev => [...prev, newItem])

// Remove from array in state
setItems(prev => prev.filter(item => item.id !== targetId))

// Update item in array
setItems(prev => prev.map(item =>
  item.id === targetId ? { ...item, ...changes } : item
))

// Redux reducer pattern — same immutable spread logic
function reducer(state = initialState, action) {
  switch (action.type) {
    case 'UPDATE_USER':
      return { ...state, user: { ...state.user, ...action.payload } }
    case 'ADD_ITEM':
      return { ...state, items: [...state.items, action.payload] }
    default:
      return state
  }
}

Common Misconceptions

⚠️

Many devs think spread creates a deep clone — but actually spread only copies one level deep. Nested objects and arrays in the spread result still reference the same objects in memory as the original. Modifying a nested property in the copy modifies the original too. Use structuredClone() for true deep copying.

⚠️

Many devs think rest parameters and the arguments object are equivalent — but actually rest parameters produce a real Array with all array methods available (map, filter, reduce). The arguments object is an array-like that lacks these methods and requires Array.from(arguments) to convert. Rest also only captures the "rest" of the parameters, not all of them, and is more explicit about intent.

⚠️

Many devs think spread can spread objects into function calls — but actually you can only spread iterables (arrays, strings, Sets, Maps, generators) as function arguments. Object spread ({ ...obj }) only works in object literal context. Trying to pass an object as function arguments via spread (fn(...obj)) throws a TypeError because plain objects are not iterable.

⚠️

Many devs think property order in object spread is arbitrary — but actually later properties always win when there are conflicts. { ...defaults, ...overrides } means every key in overrides takes precedence over the same key in defaults. The order is deterministic and left-to-right, which is exactly why this pattern is used for option merging and configuration layering.

⚠️

Many devs think ...rest in destructuring can appear anywhere in the pattern — but actually rest must always be the last element in a destructuring pattern. const [a, ...rest, b] = arr is a SyntaxError. The same rule applies to rest parameters in function signatures. Rest collects "everything remaining," so nothing can come after it.

⚠️

Many devs think the conditional spread pattern ({ ...(condition && { key: value }) }) is unusual or a hack — but actually it's idiomatic JavaScript used in React component props, Redux actions, API request bodies, and configuration objects throughout the ecosystem. It's the standard way to conditionally include properties without if/else statements.

Where You'll See This in Real Code

React's JSX prop spreading — <Component {...props} /> — passes all properties of the props object as individual props to the component. This is spread in object context applied to JSX attributes. It's used for wrapper components that need to pass through all props to a child without knowing which ones they are, and for higher-order components that add props to existing components.

Redux Toolkit's createSlice uses spread-based reducer patterns internally — every immer-powered mutation is compiled to spread-based immutable updates under the hood. Understanding that state = { ...state, field: newValue } is what actually runs helps debug cases where Immer's "mutation" syntax and JavaScript's copy semantics interact unexpectedly.

TypeScript's utility types like Partial<T>, Required<T>, and Omit<T, K> are typed equivalents of spread-based patterns — Partial makes all properties optional (equivalent to spreading with defaults), Omit removes properties (equivalent to rest destructuring). The runtime patterns and the TypeScript types are mirrors of each other.

API response normalization in Redux applications uses spread to merge incoming data with existing cache — const nextState = { ...state, users: { ...state.users, [action.payload.id]: action.payload } }. This pattern is so common that normalizr, a library for structuring API response data, generates code that looks exactly like this.

Styled-components and Emotion CSS-in-JS libraries use spread to compose styles — const buttonStyles = { ...baseStyles, ...primaryStyles, ...(disabled && disabledStyles) }. The cascade is explicit and predictable: each spread layer can override the previous, and the conditional spread handles states cleanly without ternaries inside style objects.

Lodash's merge(), assign(), and defaults() functions are all variations of what spread does, with different conflict-resolution rules. Understanding spread makes their differences intuitive: Object.assign (like spread) overwrites with the last value; _.merge goes deep; _.defaults only fills in missing keys. Knowing spread gives you the mental model to reason about all of them.

Interview Cheat Sheet

  • Spread (...): expands an iterable/object into individual elements — RIGHT side or inside calls
  • Rest (...): collects remaining elements into array/object — LEFT side or in params list
  • Array spread: [...a, ...b] combines; [...a] copies (shallow); fn(...a) as arguments
  • Object spread: { ...a, ...b } merges; later keys win; { ...obj } copies (shallow)
  • Spread is shallow — nested objects remain shared references
  • Rest parameters: fn(...args) — real Array, not arguments; must be last param
  • Rest in destructuring: [head, ...tail] or { name, ...rest } — must be last
  • Conditional spread: { ...(flag && { key: val }) } — idiomatic for optional properties
  • arguments vs rest: arguments is array-like, not an array; rest is a real Array
💡

How to Answer in an Interview

  • 1.The shallow copy limitation is the first follow-up question after any spread answer — address it preemptively
  • 2.The spread vs rest same-symbol different-direction explanation impresses every interviewer
  • 3.Live coding: implement pick() and omit() using rest + spread in one line each
  • 4.The conditional spread pattern { ...(isAdmin && { role }) } is surprisingly unknown — showing it stands out
  • 5.React immutable state updates are the canonical real-world spread use — map/filter/spread trio for array state
  • 6.rest parameters vs arguments object: rest is a real Array — always mention .map() availability
📖 Deep Dive Articles
Modern JavaScript: ES6+ Features Every Developer Must Know13 min read

Practice Questions

No questions tagged to this topic yet.

Tag questions in Admin → Questions by setting the "Topic Page" field to javascript-spread-rest-interview-questions.

Related Topics

JavaScript Destructuring Interview Questions
Beginner·4–6 Qs
JavaScript Array Interview Questions
Intermediate·8–12 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz