JavaScript Spread and Rest Operators

JavaScript's `...` operator is simultaneously one of the language's most elegant features and a source of confusion for developers. The same three-dot syntax performs two fundamentally different...

Key Insights

  • The ... syntax serves two opposite purposes: spread expands iterables into individual elements, while rest collects multiple elements into a single array—context determines which operation occurs
  • Spread and rest operators only perform shallow copies, meaning nested objects and arrays are copied by reference, not by value—a common source of bugs in state management
  • Rest parameters must always be the last parameter in a function signature and can only be used once, while spread can be used multiple times in the same expression

The Three Dots That Do Two Things

JavaScript’s ... operator is simultaneously one of the language’s most elegant features and a source of confusion for developers. The same three-dot syntax performs two fundamentally different operations depending on where you use it. When you’re expanding or unpacking values, it’s the spread operator. When you’re collecting or gathering values, it’s the rest operator.

Here’s the key distinction:

// Spread: expanding an array into individual elements
const numbers = [1, 2, 3];
console.log(...numbers); // 1 2 3

// Rest: collecting multiple arguments into an array
function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3)); // 6

The context is everything. Spread takes something that’s grouped together and breaks it apart. Rest takes things that are separate and groups them together. Once you internalize this inverse relationship, the syntax becomes intuitive.

The Spread Operator: Expanding Iterables

Spread operators work with any iterable—arrays, strings, sets, maps, and objects (though objects aren’t technically iterable, spread syntax still works with them). The most common use case is array manipulation.

Array Operations

Spreading arrays gives you a clean syntax for concatenation and cloning:

const fruits = ['apple', 'banana'];
const vegetables = ['carrot', 'spinach'];

// Concatenation
const food = [...fruits, ...vegetables];
// ['apple', 'banana', 'carrot', 'spinach']

// Cloning (shallow copy)
const fruitsCopy = [...fruits];

// Adding elements while copying
const moreFruits = [...fruits, 'orange', 'grape'];

This is cleaner than using concat() or slice() for copying. You can also spread arrays into function arguments:

const numbers = [5, 2, 8, 1, 9];
console.log(Math.max(...numbers)); // 9

// Equivalent to: Math.max(5, 2, 8, 1, 9)

Object Spreading

Object spreading is invaluable for creating modified copies without mutation:

const defaults = {
  theme: 'dark',
  fontSize: 14,
  notifications: true
};

const userPrefs = {
  fontSize: 16,
  language: 'es'
};

// Merge objects (later properties override earlier ones)
const config = { ...defaults, ...userPrefs };
// { theme: 'dark', fontSize: 16, notifications: true, language: 'es' }

Property order matters. Properties from objects spread later override earlier ones:

const override = { ...userPrefs, ...defaults };
// { fontSize: 14, language: 'es', theme: 'dark', notifications: true }
// fontSize is now 14, not 16

String Spreading

Spreading strings converts them into character arrays:

const word = 'hello';
const chars = [...word];
// ['h', 'e', 'l', 'l', 'o']

// Useful for removing duplicates
const unique = [...new Set('mississippi')];
// ['m', 'i', 's', 'p']

The Rest Operator: Collecting Values

While spread expands, rest collects. It’s primarily used in two contexts: function parameters and destructuring.

Rest Parameters in Functions

Rest parameters let you write variadic functions that accept any number of arguments:

function createUser(name, email, ...roles) {
  return {
    name,
    email,
    roles // roles is an array of all remaining arguments
  };
}

const user = createUser('Alice', 'alice@example.com', 'admin', 'editor', 'reviewer');
// { name: 'Alice', email: 'alice@example.com', roles: ['admin', 'editor', 'reviewer'] }

Rest parameters must be the last parameter—this won’t work:

// ❌ SyntaxError
function invalid(...rest, last) { }

// ✅ Correct
function valid(first, ...rest) { }

Array Destructuring with Rest

Rest syntax collects remaining array elements during destructuring:

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

// Skip elements with empty slots
const [head, , ...tail] = [1, 2, 3, 4];
console.log(head); // 1
console.log(tail); // [3, 4]

Object Destructuring with Rest

Extract specific properties while collecting the rest:

const user = {
  id: 1,
  name: 'Bob',
  email: 'bob@example.com',
  age: 30,
  city: 'Seattle'
};

const { id, email, ...profile } = user;
console.log(id);      // 1
console.log(email);   // 'bob@example.com'
console.log(profile); // { name: 'Bob', age: 30, city: 'Seattle' }

This is particularly useful for removing properties:

const { password, ...safeUser } = userFromDatabase;
// Send safeUser to the client without the password

Practical Use Cases and Patterns

Immutable Array Updates

When working with state management libraries like Redux or React’s useState, you need immutable updates:

// Add item to array
const todos = ['task1', 'task2'];
const withNewTodo = [...todos, 'task3'];

// Remove item by index
const index = 1;
const withoutTodo = [...todos.slice(0, index), ...todos.slice(index + 1)];

// Update item at index
const updated = [...todos.slice(0, index), 'updated task', ...todos.slice(index + 1)];

// Prepend item
const withPrepended = ['task0', ...todos];

Configuration Merging with Defaults

A common pattern in libraries and APIs:

function fetchData(url, options = {}) {
  const defaults = {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' },
    timeout: 5000
  };
  
  const config = { ...defaults, ...options };
  
  // User options override defaults
  return fetch(url, config);
}

fetchData('/api/users', { method: 'POST', timeout: 10000 });

Extracting and Forwarding Props

Common in React component composition:

function Button({ variant, size, ...htmlProps }) {
  const className = `btn btn-${variant} btn-${size}`;
  
  // Forward all other props to the button element
  return <button className={className} {...htmlProps} />;
}

// Usage: onClick, disabled, etc. are forwarded
<Button variant="primary" size="lg" onClick={handleClick} disabled={isLoading} />

Function Composition

Collect arguments and pass them along:

function logger(fn) {
  return function(...args) {
    console.log(`Calling with args:`, args);
    const result = fn(...args);
    console.log(`Result:`, result);
    return result;
  };
}

const add = (a, b) => a + b;
const loggedAdd = logger(add);
loggedAdd(2, 3); // Logs arguments and result

Common Pitfalls and Best Practices

Shallow Copy Gotcha

The biggest trap: spread only creates shallow copies. Nested objects are copied by reference:

const original = {
  name: 'Alice',
  settings: {
    theme: 'dark'
  }
};

const copy = { ...original };
copy.settings.theme = 'light';

console.log(original.settings.theme); // 'light' - Oops!

For deep copying, you need a different approach:

// Option 1: Manual deep copy for specific properties
const copy = {
  ...original,
  settings: { ...original.settings }
};

// Option 2: structuredClone (modern browsers)
const deepCopy = structuredClone(original);

// Option 3: Library (lodash)
const deepCopy = _.cloneDeep(original);

Rest Parameter Positioning

Rest must be last, and you can only have one:

// ❌ Wrong
function wrong(...rest, last) { }
function alsoWrong(...rest1, ...rest2) { }

// ✅ Correct
function correct(first, second, ...rest) { }

Performance Considerations

Spreading large arrays or objects has a cost. For performance-critical code with large datasets, consider alternatives:

// Slow for large arrays
const combined = [...array1, ...array2];

// Faster for large arrays
const combined = array1.concat(array2);

// Even better if you don't need a new array
array1.push(...array2); // Mutates array1

Object Property Ordering

Object spread maintains property order, but be aware of numeric keys:

const obj = { ...{ 2: 'two', 1: 'one', a: 'a' } };
// Numeric keys are sorted: { 1: 'one', 2: 'two', a: 'a' }

Conclusion

Spread and rest operators have become essential tools in modern JavaScript. They enable cleaner, more expressive code for common operations like copying, merging, and function argument handling. Remember that spread expands and rest collects, both perform shallow operations, and rest must always come last. Master these operators, and you’ll write more concise and maintainable JavaScript.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.