javascript-today

Spread and Rest Operators

Spread and Rest Operators (…)

The three dots ... operator in JavaScript serves two purposes: spread and rest. Despite looking identical, they do opposite things depending on where they’re used.

The Spread Operator

The spread operator expands an array or object into individual elements.

Spreading Arrays

Copying arrays:

const original = [1, 2, 3];
const copy = [...original];

copy.push(4);
console.log(original);  // [1, 2, 3] - unchanged
console.log(copy);      // [1, 2, 3, 4]

Combining arrays:

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];

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

Adding elements:

const numbers = [2, 3, 4];
const withMore = [1, ...numbers, 5, 6];

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

Passing array elements as function arguments:

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

// Same as: Math.max(1, 5, 3, 9, 2)

Spreading Objects

Copying objects:

const person = { name: "Alice", age: 25 };
const personCopy = { ...person };

personCopy.age = 26;
console.log(person.age);      // 25 - unchanged
console.log(personCopy.age);  // 26

Combining objects:

const defaults = { theme: "light", language: "en" };
const userPrefs = { language: "es", fontSize: 14 };

const settings = { ...defaults, ...userPrefs };
// { theme: "light", language: "es", fontSize: 14 }
// Later values override earlier ones

Adding/updating properties:

const user = { name: "Bob", age: 30 };
const updatedUser = { ...user, age: 31, city: "NYC" };

console.log(updatedUser);
// { name: "Bob", age: 31, city: "NYC" }

The Rest Operator

The rest operator collects multiple elements into an array. It’s used in function parameters and destructuring.

Rest Parameters in Functions

Collect remaining arguments into an array:

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

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

Combining with regular parameters:

function greet(greeting, ...names) {
  return greeting + ", " + names.join(" and ");
}

console.log(greet("Hello", "Alice"));              // "Hello, Alice"
console.log(greet("Hello", "Alice", "Bob"));       // "Hello, Alice and Bob"
console.log(greet("Hi", "Alice", "Bob", "Charlie")); 
// "Hi, Alice and Bob and Charlie"

Important: Rest parameter must be the last parameter:

// ✓ Correct
function example(first, ...rest) { }

// ✗ Wrong - rest must be last
function wrong(...rest, last) { }  // SyntaxError!

Rest in Destructuring

Array destructuring:

const [first, second, ...rest] = [1, 2, 3, 4, 5];

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

Object destructuring:

const person = {
  name: "Alice",
  age: 25,
  city: "NYC",
  country: "USA"
};

const { name, ...otherInfo } = person;

console.log(name);       // "Alice"
console.log(otherInfo);  // { age: 25, city: "NYC", country: "USA" }

Practical Examples

Example 1: Immutable Array Operations

const todos = [
  { id: 1, text: "Learn JS", done: false },
  { id: 2, text: "Build app", done: false }
];

// Add item (without mutating original)
const addTodo = (todos, newTodo) => [...todos, newTodo];

// Remove item
const removeTodo = (todos, id) => todos.filter(todo => todo.id !== id);

// Update item
const updateTodo = (todos, id, updates) =>
  todos.map(todo => 
    todo.id === id ? { ...todo, ...updates } : todo
  );

const newTodos = addTodo(todos, { id: 3, text: "Deploy", done: false });
console.log(newTodos.length);  // 3
console.log(todos.length);     // 2 - original unchanged

Example 2: Function with Optional Config

function createUser(name, email, options = {}) {
  const defaults = {
    role: "user",
    active: true,
    notifications: true
  };
  
  return {
    name,
    email,
    ...defaults,
    ...options  // Options override defaults
  };
}

const user1 = createUser("Alice", "alice@example.com");
// { name: "Alice", email: "alice@...", role: "user", active: true, ... }

const user2 = createUser("Bob", "bob@example.com", { role: "admin", active: false });
// { name: "Bob", email: "bob@...", role: "admin", active: false, ... }

Example 3: Logger with Variable Arguments

function log(level, ...messages) {
  const timestamp = new Date().toISOString();
  const formatted = messages.join(" ");
  console.log(`[${timestamp}] ${level}: ${formatted}`);
}

log("INFO", "User", "logged", "in");
// [2024-01-15T10:30:00.000Z] INFO: User logged in

log("ERROR", "Failed", "to", "save", "data");
// [2024-01-15T10:30:01.000Z] ERROR: Failed to save data

Example 4: React State Updates

// Common pattern in React
const [user, setUser] = useState({ name: "Alice", age: 25 });

// Update one property immutably
setUser({ ...user, age: 26 });

// Or with prev state
setUser(prev => ({ ...prev, age: prev.age + 1 }));

Shallow vs Deep Copy

Warning: Spread creates shallow copies only:

const original = {
  name: "Alice",
  address: { city: "NYC", country: "USA" }
};

const copy = { ...original };

copy.address.city = "LA";  // This changes original too!

console.log(original.address.city);  // "LA" - uh oh!

For nested objects, you need deep cloning:

// Simple deep clone (loses functions, dates, etc.)
const deepCopy = JSON.parse(JSON.stringify(original));

// Or use a library like lodash
const deepCopy = _.cloneDeep(original);

When to Use Spread vs Rest

Use Case Operator Example
Copy array Spread [...arr]
Copy object Spread {...obj}
Merge arrays Spread [...arr1, ...arr2]
Merge objects Spread {...obj1, ...obj2}
Function args Spread fn(...args)
Collect params Rest function(...args) {}
Destructure array Rest [first, ...rest] = arr
Destructure object Rest {prop, ...rest} = obj

Combining Spread and Rest

function logUserAction(action, user, ...details) {
  const logEntry = {
    timestamp: Date.now(),
    action,
    user: { ...user },  // Copy user to avoid mutations
    details: [...details]  // Collect remaining args
  };
  
  console.log(logEntry);
}

const user = { id: 1, name: "Alice" };
logUserAction("login", user, "192.168.1.1", "Chrome");

Common Patterns

1. Remove property from object:

const user = { id: 1, name: "Alice", password: "secret123" };
const { password, ...safeUser } = user;

console.log(safeUser);  // { id: 1, name: "Alice" }

2. Conditional properties:

const createUser = (name, isAdmin) => ({
  name,
  createdAt: new Date(),
  ...(isAdmin && { role: "admin", permissions: ["read", "write"] })
});

console.log(createUser("Alice", true));
// { name: "Alice", createdAt: ..., role: "admin", permissions: [...] }

console.log(createUser("Bob", false));
// { name: "Bob", createdAt: ... }

3. Function composition:

const compose = (...fns) => (value) =>
  fns.reduceRight((acc, fn) => fn(acc), value);

const double = x => x * 2;
const addOne = x => x + 1;
const square = x => x * x;

const compute = compose(square, addOne, double);
console.log(compute(3));  // ((3 * 2) + 1)² = 49

Browser Support

Spread and rest are ES6 features, supported in:

  • All modern browsers
  • Node.js 6+
  • Can be transpiled with Babel for older environments