Spread and rest share the same ... syntax but do opposite things: spread expands an array or object in place, rest collects multiple values into one. The practical payoff in test automation is test data factories: one base object spread with overrides creates every variant without repeating all fields. This guide covers both operators with the patterns that appear in Playwright configs, fixture definitions, and request headers, plus the shallow-copy trap that corrupts test data when nested objects are shared between variants.

Spread Operator: Expanding Things

The spread operator (...) expands an iterable (array or object) in place.

Spreading arrays

const part1 = [1, 2, 3];
const part2 = [4, 5, 6];

const combined = [...part1, ...part2];
// [1, 2, 3, 4, 5, 6]

// Add items before or after
const withExtra = [0, ...part1, ...part2, 7];
// [0, 1, 2, 3, 4, 5, 6, 7]

Copying an array

const original = ['alice', 'bob', 'charlie'];
const copy = [...original];

copy.push('dave'); // Only modifies copy
console.log(original); // ['alice', 'bob', 'charlie'] — unchanged

Spreading objects

const baseConfig = { timeout: 30000, headless: true };
const ciConfig = { ...baseConfig, timeout: 60000 };
// { timeout: 60000, headless: true }
// Note: timeout was overridden by the later value

The later key wins when there's a conflict.

Spread in Test Data: The Big Use Case

The most valuable pattern for QA engineers: creating object variants from a base:

const defaultUser = {
  email: 'test@example.com',
  password: 'ValidPass1',
  role: 'member',
  isActive: true,
  emailVerified: true,
};

// Create variants without repeating all fields
const adminUser    = { ...defaultUser, role: 'admin' };
const inactiveUser = { ...defaultUser, isActive: false };
const unverified   = { ...defaultUser, emailVerified: false };
const customEmail  = { ...defaultUser, email: 'custom@test.com' };

This is the foundation of test data factories. Define the happy-path state once, create all your edge cases from it.

Merging test configs

const basePlaywrightConfig = {
  use: {
    baseURL: 'https://lab.becomeqa.com',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  timeout: 30_000,
};

// CI environment overrides
const ciOverrides = {
  timeout: 60_000,
  retries: 2,
};

const finalConfig = { ...basePlaywrightConfig, ...ciOverrides };

Building test case lists

const happyPathCase = {
  email: 'valid@example.com',
  password: 'ValidPass1',
  expectedResult: 'success',
  expectedStatus: 200,
};

const testCases = [
  happyPathCase,
  { ...happyPathCase, email: 'another@test.com' },
  { ...happyPathCase, password: 'AnotherValidPass2' },
  { ...happyPathCase, email: 'not-valid', expectedResult: 'error', expectedStatus: 422 },
];

Much cleaner than repeating all fields for every test case.

Rest Parameters: Collecting Things

Rest parameters collect multiple items into a single parameter. It uses the same ... syntax but in a function definition, not a call:

// REST: collects multiple arguments into an array
function logTestResults(testName: string, ...results: string[]) {
  console.log(`Test: ${testName}`);
  results.forEach(r => console.log(`  - ${r}`));
}

logTestResults('Login test', 'email validated', 'redirect worked', 'session created');
// Test: Login test
//   - email validated
//   - redirect worked
//   - session created

The ...results collects all arguments after testName into an array.

Rest in destructuring

Rest can also be used in destructuring to collect "everything else":

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

const { email, password, ...otherFields } = user;
// email and password are extracted
// otherFields = { role: 'member', isActive: true, ... }

This is useful when you need specific fields and want to pass the rest along:

async function createUserAndGetToken({ email, password, ...profileData }: UserCreationData) {
  // Use email and password for auth
  const token = await auth.login(email, password);
  
  // Use the rest for profile setup (without auth fields)
  await api.updateProfile(token, profileData);
  
  return token;
}

Practical Patterns in Playwright

Pattern 1: Flexible test helper

type ClickOptions = {
  timeout?: number;
  force?: boolean;
};

async function clickAndWait(
  page: Page,
  selector: string,
  { timeout = 5000, force = false, ...options }: ClickOptions = {}
) {
  await page.locator(selector).click({ timeout, force, ...options });
  await page.waitForLoadState('networkidle');
}

Pattern 2: Building request headers

const defaultHeaders = {
  'Content-Type': 'application/json',
  'Accept': 'application/json',
};

const authHeaders = {
  ...defaultHeaders,
  'Authorization': `Bearer ${token}`,
};

const adminHeaders = {
  ...authHeaders,
  'X-Admin-Key': process.env.ADMIN_KEY,
};

Pattern 3: Combining test data from multiple sources

const baseData = await readFixtureFile('base-user.json');
const envSpecific = await readFixtureFile(`${process.env.ENV}-overrides.json`);

const testData = { ...baseData, ...envSpecific };
// Environment-specific values override base values

Pattern 4: Collecting failed cases

async function runAllCases(cases: TestCase[]): Promise<string[]> {
  const failures: string[] = [];
  
  for (const testCase of cases) {
    try {
      await runCase(testCase);
    } catch (error) {
      failures.push(`${testCase.name}: ${error.message}`);
    }
  }
  
  return failures;
}

// Call site:
const [firstFailure, ...otherFailures] = await runAllCases(testCases);
if (firstFailure) {
  console.log('First failure:', firstFailure);
  console.log('Additional failures:', otherFailures);
}

Spread vs. Rest: Tell Them Apart

Spread — you're putting things in (expanding into an array/object literal):

const arr = [...items];         // expanding items into array
const obj = { ...config };      // expanding config into object
func(...args);                   // expanding args as function arguments

Rest — you're collecting things from (gathering into a parameter):

function fn(...args) {}          // collecting call arguments
const [a, ...rest] = array;     // collecting remainder of array
const { x, ...others } = obj;   // collecting remainder of object

Same ... symbol. Context determines the meaning.

A Common Mistake: Shallow Copy

Spread creates a shallow copy — nested objects are still shared references:

const user = { name: 'Alice', address: { city: 'NYC' } };
const copy = { ...user };

copy.name = 'Bob';             // ✅ Only changes copy
copy.address.city = 'London'; // ⚠️ Changes BOTH user and copy

console.log(user.address.city); // 'London' — oops

If you need a deep copy (nested objects are also independent), you need a different approach:

// Simple deep copy (works for JSON-serializable data)
const deepCopy = JSON.parse(JSON.stringify(user));

// Or use structuredClone (modern JS)
const deepCopy2 = structuredClone(user);

For most test data objects that are simple key-value pairs without nested mutable objects, spread is fine. But know the limitation.

Summary

| Syntax | Context | What it does |

|--------|---------|-------------|

| [...arr] | Array literal | Expands arr into the new array |

| {...obj} | Object literal | Copies all properties of obj |

| fn(...args) | Function call | Passes array elements as separate arguments |

| function fn(...params) | Function definition | Collects multiple arguments into array |

| const [a, ...rest] = arr | Array destructuring | Collects remaining items |

| const {x, ...rest} = obj | Object destructuring | Collects remaining properties |

Once the pattern clicks, you'll recognize it everywhere — in Playwright's use(), in test fixtures, in config files, and in every test data factory you'll ever read.

→ See also: JavaScript Objects and Destructuring for QA Engineers | JavaScript for QA Engineers: The Minimum You Need to Start Automating | TypeScript for QA: Why Static Types Make Your Tests Better