JavaScript Essentials

Spread & Rest Operators

45 min Lesson 19 of 60

Introduction to the Three Dots Syntax

JavaScript ES6 introduced a powerful syntax represented by three dots (...) that serves two distinct purposes depending on how it is used. When used to expand or unpack elements, it is called the spread operator. When used to collect or gather elements, it is called the rest parameter. Despite sharing the same syntax, these two features solve very different problems. Understanding the spread and rest operators is essential for writing modern JavaScript. They simplify array manipulation, object composition, function signatures, and destructuring patterns. By the end of this lesson, you will know exactly when and how to use each one effectively in your code.

The Spread Operator with Arrays

The spread operator expands an iterable (like an array) into individual elements. Think of it as "unpacking" the contents of an array so each element stands on its own. This is incredibly useful for copying arrays, merging arrays, and passing array elements as function arguments.

Copying Arrays

Before the spread operator, developers used methods like Array.prototype.slice() or Array.from() to create shallow copies of arrays. The spread operator provides a cleaner, more readable way to achieve the same result. When you spread an array into a new array literal, you create a brand new array with all the same elements. Changes to the original array will not affect the copy, and changes to the copy will not affect the original.

Example: Copying an Array with Spread

const originalFruits = ['apple', 'banana', 'cherry'];
const copiedFruits = [...originalFruits];

console.log(copiedFruits);
// Output: ['apple', 'banana', 'cherry']

// Modifying the copy does not affect the original
copiedFruits.push('date');
console.log(originalFruits); // ['apple', 'banana', 'cherry']
console.log(copiedFruits);   // ['apple', 'banana', 'cherry', 'date']

// Compare with the old approach
const oldCopy = originalFruits.slice();
console.log(oldCopy); // ['apple', 'banana', 'cherry']
Important: The spread operator creates a shallow copy, not a deep copy. If your array contains objects or nested arrays, those inner references are shared between the original and the copy. Modifying a nested object in the copy will also modify it in the original. For deep copies, use structuredClone() or a library like Lodash.

Example: Shallow Copy Limitation

const users = [
    { name: 'Alice', age: 30 },
    { name: 'Bob', age: 25 }
];

const usersCopy = [...users];

// The outer array is a new reference
console.log(users === usersCopy); // false

// But the inner objects are still shared references
console.log(users[0] === usersCopy[0]); // true

usersCopy[0].age = 31;
console.log(users[0].age); // 31 -- original is also changed!

// For a true deep copy, use structuredClone
const deepCopy = structuredClone(users);
deepCopy[0].age = 99;
console.log(users[0].age); // 31 -- original is NOT affected

Merging Arrays

The spread operator makes merging two or more arrays intuitive and readable. You simply spread each array inside a new array literal. This replaces the older Array.prototype.concat() method with a more visual and flexible syntax. You can also insert individual elements between the spread arrays, giving you full control over the resulting order.

Example: Merging Arrays

const frontend = ['HTML', 'CSS', 'JavaScript'];
const backend = ['Node.js', 'Python', 'Go'];
const databases = ['PostgreSQL', 'MongoDB'];

// Merge two arrays
const fullstack = [...frontend, ...backend];
console.log(fullstack);
// ['HTML', 'CSS', 'JavaScript', 'Node.js', 'Python', 'Go']

// Merge with additional elements in between
const allSkills = [...frontend, 'React', 'Vue', ...backend, ...databases];
console.log(allSkills);
// ['HTML', 'CSS', 'JavaScript', 'React', 'Vue', 'Node.js', 'Python', 'Go', 'PostgreSQL', 'MongoDB']

// Compare with the old concat approach
const oldMerge = frontend.concat(backend).concat(databases);
console.log(oldMerge);
// ['HTML', 'CSS', 'JavaScript', 'Node.js', 'Python', 'Go', 'PostgreSQL', 'MongoDB']

Passing Array Elements to Functions

Before the spread operator, if you had an array of values and wanted to pass them as individual arguments to a function, you had to use Function.prototype.apply(). The spread operator eliminates this need entirely. You can spread an array directly inside a function call, and each element becomes a separate argument.

Example: Spreading into Function Arguments

const numbers = [5, 12, 3, 8, 1, 19, 7];

// Find the maximum value
const max = Math.max(...numbers);
console.log(max); // 19

// Find the minimum value
const min = Math.min(...numbers);
console.log(min); // 1

// Old approach using apply
const oldMax = Math.max.apply(null, numbers);
console.log(oldMax); // 19

// Spread works with any function that takes multiple arguments
function calculateTotal(price, tax, shipping) {
    return price + tax + shipping;
}

const costs = [49.99, 4.50, 5.99];
const total = calculateTotal(...costs);
console.log(total); // 60.48

// You can mix spread with regular arguments
const moreCosts = [4.50, 5.99];
const totalMixed = calculateTotal(49.99, ...moreCosts);
console.log(totalMixed); // 60.48

The Spread Operator with Objects

ES2018 extended the spread syntax to work with objects. Spreading an object copies all of its own enumerable properties into a new object. This is extremely useful for copying objects, merging objects, and creating modified versions of objects without mutating the originals.

Copying Objects

Just like with arrays, spreading an object into a new object literal creates a shallow copy. This is functionally equivalent to Object.assign({}, original), but the spread syntax is more concise and easier to read.

Example: Copying an Object with Spread

const userProfile = {
    name: 'Sarah',
    email: 'sarah@example.com',
    role: 'developer',
    active: true
};

const profileCopy = { ...userProfile };
console.log(profileCopy);
// { name: 'Sarah', email: 'sarah@example.com', role: 'developer', active: true }

// The copy is a new object
console.log(userProfile === profileCopy); // false

// Modifying the copy does not affect the original
profileCopy.role = 'senior developer';
console.log(userProfile.role);  // 'developer'
console.log(profileCopy.role);  // 'senior developer'

Merging Objects

Spreading multiple objects into a single object literal merges their properties. When two objects have the same property name, the last one wins. This order-dependent behavior is predictable and very useful for combining configuration objects, default values, and user preferences.

Example: Merging Objects

const defaultSettings = {
    theme: 'light',
    fontSize: 14,
    language: 'en',
    notifications: true,
    autoSave: false
};

const userSettings = {
    theme: 'dark',
    fontSize: 18,
    autoSave: true
};

// User settings override defaults
const finalSettings = { ...defaultSettings, ...userSettings };
console.log(finalSettings);
// {
//     theme: 'dark',          -- overridden by userSettings
//     fontSize: 18,            -- overridden by userSettings
//     language: 'en',         -- kept from defaultSettings
//     notifications: true,     -- kept from defaultSettings
//     autoSave: true           -- overridden by userSettings
// }

// Three-way merge
const adminOverrides = { notifications: false, debugMode: true };
const adminSettings = { ...defaultSettings, ...userSettings, ...adminOverrides };
console.log(adminSettings);
// { theme: 'dark', fontSize: 18, language: 'en', notifications: false, autoSave: true, debugMode: true }

Overriding Specific Properties

A very common pattern is to spread an existing object and then override one or more specific properties. This creates a new object with all the original properties plus your changes, without mutating the original. This is the foundation of immutable state updates in frameworks like React.

Example: Overriding Properties

const product = {
    id: 101,
    name: 'Wireless Headphones',
    price: 79.99,
    inStock: true,
    category: 'electronics'
};

// Create an updated version with a new price
const updatedProduct = { ...product, price: 59.99 };
console.log(updatedProduct);
// { id: 101, name: 'Wireless Headphones', price: 59.99, inStock: true, category: 'electronics' }

// Add new properties while copying
const enrichedProduct = {
    ...product,
    discount: 0.25,
    finalPrice: product.price * 0.75,
    tags: ['audio', 'wireless', 'bluetooth']
};
console.log(enrichedProduct.finalPrice); // 59.9925

// Original is unchanged
console.log(product.price); // 79.99
console.log(product.discount); // undefined

Spread with Strings

Strings are iterable in JavaScript, so you can spread a string into an array to get an array of individual characters. This is a clean alternative to String.prototype.split('') and works correctly with most Unicode characters.

Example: Spreading Strings

const greeting = 'Hello';
const chars = [...greeting];
console.log(chars); // ['H', 'e', 'l', 'l', 'o']

// Useful for string manipulation
const word = 'JavaScript';
const reversed = [...word].reverse().join('');
console.log(reversed); // 'tpircSavaJ'

// Counting unique characters
const sentence = 'hello world';
const uniqueChars = [...new Set(sentence)];
console.log(uniqueChars);
// ['h', 'e', 'l', 'o', ' ', 'w', 'r', 'd']

// Works with Unicode emoji
const emoji = 'Hello! 🌍🚀';
const emojiChars = [...emoji];
console.log(emojiChars);
// ['H', 'e', 'l', 'l', 'o', '!', ' ', '🌍', '🚀']
// Note: split('') would break multi-byte emoji into garbage characters

Rest Parameters in Functions

The rest parameter syntax looks identical to the spread operator -- three dots followed by a name -- but it does the opposite. Instead of expanding elements, it collects them. When used in a function parameter list, the rest parameter gathers all remaining arguments into a single array. This is far more flexible and modern than the old arguments object.

Example: Basic Rest Parameters

// The rest parameter collects all arguments into an array
function sum(...numbers) {
    return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3));        // 6
console.log(sum(10, 20, 30, 40)); // 100
console.log(sum(5));               // 5
console.log(sum());                // 0

// Rest parameter is a real array (unlike arguments)
function showType(...args) {
    console.log(Array.isArray(args));     // true
    console.log(args.map);                // function map() { ... }
}

showType(1, 2, 3);

Combining Regular Parameters with Rest

You can use regular parameters before the rest parameter to capture specific arguments, while the rest parameter collects everything else. The rest parameter must always be the last parameter in the function signature. This pattern is very useful when the first few arguments have specific meanings but the remaining count is variable.

Example: Rest with Leading Parameters

function createTeam(teamName, captain, ...members) {
    console.log(`Team: ${teamName}`);
    console.log(`Captain: ${captain}`);
    console.log(`Members: ${members.join(', ')}`);
    console.log(`Total size: ${members.length + 1}`);
}

createTeam('Alpha', 'Alice', 'Bob', 'Charlie', 'Diana');
// Team: Alpha
// Captain: Alice
// Members: Bob, Charlie, Diana
// Total size: 4

// A logging function with a severity level
function log(level, ...messages) {
    const timestamp = new Date().toISOString();
    const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
    messages.forEach(msg => console.log(`${prefix} ${msg}`));
}

log('info', 'Server started', 'Listening on port 3000');
// [2025-01-15T10:30:00.000Z] [INFO] Server started
// [2025-01-15T10:30:00.000Z] [INFO] Listening on port 3000

log('error', 'Database connection failed');
// [2025-01-15T10:30:00.000Z] [ERROR] Database connection failed
Note: The rest parameter must be the last parameter in a function definition. Writing function foo(...rest, lastParam) will throw a SyntaxError. There can also be only one rest parameter per function.

Rest Parameters vs the Arguments Object

Before rest parameters existed, JavaScript provided the arguments object inside every function. While arguments still works, rest parameters are superior in every way. The arguments object is not a real array, so you cannot use array methods like map, filter, or reduce directly on it. Rest parameters give you a genuine array from the start.

Example: Rest Parameters vs Arguments

// Old way: arguments object (avoid this)
function oldSum() {
    // arguments is array-like, not a real array
    console.log(Array.isArray(arguments)); // false

    // Must convert to array first
    const args = Array.from(arguments);
    return args.reduce((total, num) => total + num, 0);
}

// Modern way: rest parameters (use this)
function modernSum(...numbers) {
    // numbers is a real array
    console.log(Array.isArray(numbers)); // true

    return numbers.reduce((total, num) => total + num, 0);
}

console.log(oldSum(1, 2, 3));    // 6
console.log(modernSum(1, 2, 3)); // 6

// Arrow functions do NOT have arguments object
// Rest parameters are the only option
const arrowSum = (...numbers) => numbers.reduce((a, b) => a + b, 0);
console.log(arrowSum(10, 20, 30)); // 60

Rest in Destructuring

The rest syntax also works inside destructuring assignments for both arrays and objects. When destructuring, the rest element collects all remaining items that were not explicitly assigned to individual variables. This is a powerful pattern for separating the first few items from the rest, or for extracting specific properties while keeping the others grouped together.

Array Destructuring with Rest

Example: Rest in Array Destructuring

const scores = [95, 88, 76, 92, 85, 71, 90];

// Get first two scores, collect the rest
const [highest, secondHighest, ...remaining] = scores;
console.log(highest);       // 95
console.log(secondHighest); // 88
console.log(remaining);     // [76, 92, 85, 71, 90]

// Useful for head/tail pattern
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]

// Skipping elements with rest
const [first, , third, ...others] = ['a', 'b', 'c', 'd', 'e'];
console.log(first);  // 'a'
console.log(third);  // 'c'
console.log(others); // ['d', 'e']

Object Destructuring with Rest

Example: Rest in Object Destructuring

const user = {
    id: 42,
    name: 'Alex',
    email: 'alex@example.com',
    role: 'admin',
    department: 'engineering',
    hireDate: '2023-01-15'
};

// Extract specific props, collect the rest
const { id, name, ...details } = user;
console.log(id);      // 42
console.log(name);    // 'Alex'
console.log(details);
// { email: 'alex@example.com', role: 'admin', department: 'engineering', hireDate: '2023-01-15' }

// Practical use: removing a property from an object immutably
const { role, ...userWithoutRole } = user;
console.log(userWithoutRole);
// { id: 42, name: 'Alex', email: 'alex@example.com', department: 'engineering', hireDate: '2023-01-15' }
// role has been excluded without mutating the original

// Extracting sensitive data
const apiResponse = {
    data: { items: [1, 2, 3] },
    status: 200,
    headers: { 'content-type': 'application/json' },
    config: { timeout: 5000 },
    request: { method: 'GET' }
};

const { data, status, ...internals } = apiResponse;
console.log(data);       // { items: [1, 2, 3] }
console.log(status);     // 200
console.log(internals);  // { headers: {...}, config: {...}, request: {...} }
Pro Tip: The rest-in-destructuring pattern is the cleanest way to remove properties from an object without mutation. Instead of using delete obj.prop (which mutates), destructure with rest: const { propToRemove, ...objectWithoutProp } = originalObject;. This is a common pattern in Redux reducers and React state management.

Spread vs Rest: Same Syntax, Different Purpose

The three dots (...) have two completely opposite behaviors depending on context. Understanding the difference is crucial for reading and writing modern JavaScript. The key distinction is simple: spread expands and rest collects.

Example: Spread vs Rest Side by Side

// SPREAD: Expands an array into individual elements
// Used in array literals, object literals, and function calls
const parts = [3, 4, 5];
const complete = [1, 2, ...parts, 6]; // SPREAD: expanding parts
console.log(complete); // [1, 2, 3, 4, 5, 6]

Math.max(...parts); // SPREAD: expanding into arguments
// equivalent to Math.max(3, 4, 5)


// REST: Collects individual elements into an array
// Used in function parameters and destructuring patterns
function demo(first, ...rest) { // REST: collecting arguments
    console.log(first); // individual value
    console.log(rest);  // array of remaining values
}
demo(1, 2, 3, 4); // first=1, rest=[2, 3, 4]

const [a, ...others] = [10, 20, 30]; // REST: collecting remainder
console.log(a);      // 10
console.log(others); // [20, 30]


// BOTH in one line
function merge(...arrays) {          // REST: collect all arguments
    return [].concat(...arrays);      // SPREAD: expand each array
}
console.log(merge([1, 2], [3, 4], [5])); // [1, 2, 3, 4, 5]
Note: A simple way to remember: if ... appears on the right side of an assignment or inside a function call, it is spread (expanding). If it appears on the left side of an assignment or in a function parameter definition, it is rest (collecting).

Practical Patterns

Now that you understand both spread and rest, let us explore practical patterns that combine these features to solve real-world problems. These patterns appear constantly in modern JavaScript codebases, libraries, and frameworks.

Immutable Array Updates

When working with state in frameworks like React, you should never mutate arrays directly. The spread operator gives you tools to add, remove, and update items in arrays without mutation.

Example: Immutable Array Operations

const todos = [
    { id: 1, text: 'Learn HTML', done: true },
    { id: 2, text: 'Learn CSS', done: true },
    { id: 3, text: 'Learn JavaScript', done: false }
];

// Add an item to the end
const withNewTodo = [...todos, { id: 4, text: 'Learn React', done: false }];

// Add an item to the beginning
const withFirstTodo = [{ id: 0, text: 'Setup environment', done: true }, ...todos];

// Remove an item by id (filter does not mutate)
const withoutSecond = todos.filter(todo => todo.id !== 2);

// Update a specific item without mutation
const withUpdated = todos.map(todo =>
    todo.id === 3
        ? { ...todo, done: true }  // spread + override
        : todo
);
console.log(withUpdated[2]);
// { id: 3, text: 'Learn JavaScript', done: true }

// Insert at a specific index
const index = 2;
const withInserted = [
    ...todos.slice(0, index),
    { id: 99, text: 'Learn TypeScript', done: false },
    ...todos.slice(index)
];
console.log(withInserted.map(t => t.text));
// ['Learn HTML', 'Learn CSS', 'Learn TypeScript', 'Learn JavaScript']

Immutable Object Updates

The same immutability principle applies to objects. Spread lets you create new versions of objects with updated properties, which is the core of state management in modern frontend development.

Example: Immutable Object Operations

const state = {
    user: { name: 'Alex', email: 'alex@example.com' },
    theme: 'dark',
    notifications: { email: true, push: false, sms: false },
    lastLogin: '2025-01-15'
};

// Update a top-level property
const newState1 = { ...state, theme: 'light' };

// Update a nested object (must spread at each level)
const newState2 = {
    ...state,
    notifications: {
        ...state.notifications,
        push: true
    }
};
console.log(newState2.notifications);
// { email: true, push: true, sms: false }

// Update deeply nested property
const newState3 = {
    ...state,
    user: {
        ...state.user,
        email: 'newemail@example.com'
    }
};
console.log(newState3.user.email); // 'newemail@example.com'
console.log(state.user.email);     // 'alex@example.com' -- unchanged

Function Wrappers and Decorators

Rest parameters and spread work together beautifully in function wrappers. You can collect all arguments with rest, forward them to another function with spread, and add extra behavior around the call. This is the pattern behind logging decorators, performance timers, retry logic, and middleware.

Example: Function Wrappers

// A timing wrapper that measures execution time
function withTiming(fn) {
    return function (...args) {          // REST: collect all arguments
        const start = performance.now();
        const result = fn(...args);       // SPREAD: forward all arguments
        const end = performance.now();
        console.log(`${fn.name} took ${(end - start).toFixed(2)}ms`);
        return result;
    };
}

function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

const timedFib = withTiming(fibonacci);
timedFib(30); // fibonacci took 12.45ms (example)


// A retry wrapper for async functions
function withRetry(fn, maxRetries = 3) {
    return async function (...args) {
        for (let attempt = 1; attempt <= maxRetries; attempt++) {
            try {
                return await fn(...args);
            } catch (error) {
                if (attempt === maxRetries) throw error;
                console.log(`Attempt ${attempt} failed, retrying...`);
            }
        }
    };
}

async function fetchData(url, options) {
    const response = await fetch(url, options);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
}

const resilientFetch = withRetry(fetchData, 3);
// resilientFetch('https://api.example.com/data', { method: 'GET' });


// A memoization wrapper
function memoize(fn) {
    const cache = new Map();
    return function (...args) {
        const key = JSON.stringify(args);
        if (cache.has(key)) {
            console.log('Cache hit!');
            return cache.get(key);
        }
        const result = fn(...args);
        cache.set(key, result);
        return result;
    };
}

const memoizedAdd = memoize((a, b) => a + b);
console.log(memoizedAdd(1, 2)); // 3
console.log(memoizedAdd(1, 2)); // Cache hit! 3

Real-World Examples

Let us put everything together with comprehensive real-world examples that you are likely to encounter in production JavaScript applications.

Building a Flexible API Client

Example: API Client with Spread and Rest

// Default configuration that can be overridden
const defaultConfig = {
    baseURL: 'https://api.example.com',
    timeout: 5000,
    headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    }
};

function createApiClient(customConfig = {}) {
    // Merge configs, with custom overriding defaults
    const config = {
        ...defaultConfig,
        ...customConfig,
        headers: {
            ...defaultConfig.headers,
            ...customConfig.headers
        }
    };

    return {
        async get(endpoint, ...queryParams) {
            const queryString = queryParams
                .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
                .join('&');
            const url = `${config.baseURL}${endpoint}?${queryString}`;
            console.log(`GET ${url}`);
            // In real code: return fetch(url, { headers: config.headers });
        },

        async post(endpoint, data, ...middlewares) {
            let processedData = { ...data };
            for (const middleware of middlewares) {
                processedData = middleware(processedData);
            }
            console.log(`POST ${config.baseURL}${endpoint}`, processedData);
        }
    };
}

const api = createApiClient({
    headers: { 'Authorization': 'Bearer token123' }
});

api.get('/users', ['page', '1'], ['limit', '10']);
// GET https://api.example.com/users?page=1&limit=10

const addTimestamp = (data) => ({ ...data, timestamp: Date.now() });
const addVersion = (data) => ({ ...data, version: '1.0' });

api.post('/users', { name: 'Alex' }, addTimestamp, addVersion);
// POST https://api.example.com/users { name: 'Alex', timestamp: 1705312200000, version: '1.0' }

Array and Object Utility Functions

Example: Utility Functions Using Spread and Rest

// Remove duplicates from multiple arrays
function uniqueMerge(...arrays) {
    return [...new Set(arrays.flat())];
}
console.log(uniqueMerge([1, 2, 3], [2, 3, 4], [4, 5, 6]));
// [1, 2, 3, 4, 5, 6]

// Pick specific properties from an object
function pick(obj, ...keys) {
    return keys.reduce((result, key) => {
        if (key in obj) {
            result[key] = obj[key];
        }
        return result;
    }, {});
}

const fullUser = { id: 1, name: 'Alex', email: 'a@b.com', password: 'secret', role: 'admin' };
const safeUser = pick(fullUser, 'id', 'name', 'email');
console.log(safeUser); // { id: 1, name: 'Alex', email: 'a@b.com' }

// Omit specific properties from an object
function omit(obj, ...keys) {
    const keysToRemove = new Set(keys);
    return Object.fromEntries(
        Object.entries(obj).filter(([key]) => !keysToRemove.has(key))
    );
}

const publicUser = omit(fullUser, 'password', 'role');
console.log(publicUser); // { id: 1, name: 'Alex', email: 'a@b.com' }

// Compose multiple functions into one
function compose(...fns) {
    return function (value) {
        return fns.reduceRight((acc, fn) => fn(acc), value);
    };
}

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

const transform = compose(square, addOne, double);
console.log(transform(3)); // square(addOne(double(3))) = square(7) = 49

// Partition an array into two groups
function partition(array, predicate) {
    return array.reduce(
        ([pass, fail], item) =>
            predicate(item)
                ? [[...pass, item], fail]
                : [pass, [...fail, item]],
        [[], []]
    );
}

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const [evens, odds] = partition(numbers, n => n % 2 === 0);
console.log(evens); // [2, 4, 6, 8, 10]
console.log(odds);  // [1, 3, 5, 7, 9]

Event Handler Patterns

Example: Event System with Spread and Rest

class EventEmitter {
    constructor() {
        this.listeners = {};
    }

    on(event, ...callbacks) {
        if (!this.listeners[event]) {
            this.listeners[event] = [];
        }
        this.listeners[event] = [...this.listeners[event], ...callbacks];
        return this;
    }

    emit(event, ...args) {
        const handlers = this.listeners[event] || [];
        handlers.forEach(handler => handler(...args));
        return this;
    }

    off(event, callback) {
        if (this.listeners[event]) {
            this.listeners[event] = this.listeners[event]
                .filter(cb => cb !== callback);
        }
        return this;
    }
}

const emitter = new EventEmitter();

emitter.on('userLogin',
    (user) => console.log(`Welcome, ${user.name}!`),
    (user) => console.log(`Last login: ${user.lastLogin}`)
);

emitter.emit('userLogin', { name: 'Alex', lastLogin: '2025-01-14' });
// Welcome, Alex!
// Last login: 2025-01-14
Common Mistake: Trying to spread a non-iterable value like a number or null into an array will throw a TypeError. Always ensure the value you are spreading is iterable (arrays, strings, Sets, Maps) or an object (for object spread). Use a fallback pattern like [...(items || [])] or { ...(config || {}) } when the value might be null or undefined.

Practice Exercise

Build a small task management module using only spread and rest operators for all data transformations. Create a createTaskManager function that returns an object with methods: addTasks(...newTasks) that accepts any number of task objects and adds them to the internal list immutably, completeTask(id) that marks a task as done without mutating the original array, removeTasks(...ids) that removes multiple tasks by their IDs, getStats() that returns an object with total, completed, and pending counts, and mergeLists(...taskLists) that merges multiple task arrays and removes duplicates by ID. Use the spread operator for all array and object copies, use rest parameters for variable-length argument lists, use destructuring with rest to extract and separate data, and never use push, splice, delete, or any mutating method. Test your module by creating tasks, completing some, removing others, merging multiple lists, and verifying that original data is never modified.