TypeScript Spread Operator
The TypeScript spread operator (...) takes elements from arrays or properties from objects and expands them into a new array or object. It's one of those features that makes your code cleaner once you understand it, but it comes with some TypeScript-specific quirks around type safety that can trip you up.
This guide covers practical patterns for using the spread operator, how to handle type inference issues, and when you should reach for an alternative.
Using the TypeScript Spread Operator with Arrays
The spread operator expands an array into individual elements. Here's the basic syntax:
const existingIds = [1, 2, 3];
const allIds = [...existingIds, 4, 5, 6];
console.log(allIds); // [1, 2, 3, 4, 5, 6]
This is cleaner than using concat() and makes your intent obvious at a glance.
Merging Arrays
When you need to combine multiple arrays, the spread operator handles it without loops or helper methods.
const activeUsers = [1, 2, 3];
const pendingUsers = [4, 5, 6];
const allUsers = [...activeUsers, ...pendingUsers];
console.log(allUsers); // [1, 2, 3, 4, 5, 6]
This approach works well with typescript array operations when you're joining data from different sources.
Cloning Arrays
The spread operator creates a shallow copy that's separate from the original array.
const originalTags = ['typescript', 'javascript', 'react'];
const copiedTags = [...originalTags];
console.log(copiedTags); // ['typescript', 'javascript', 'react']
When working with complex objects in a Convex database, shallow copying is helpful for creating modified versions of your data before updating it. Check out argument validation without repetition for best practices with objects in Convex.
Using Spread for Array Destructuring
The spread operator can gather remaining elements into a new array when destructuring.
const [currentTask, ...remainingTasks] = [
'review-pr',
'update-docs',
'fix-bug',
'deploy'
];
console.log(currentTask); // 'review-pr'
console.log(remainingTasks); // ['update-docs', 'fix-bug', 'deploy']
This pattern is useful when processing data from Convex queries where you might need to separate the first item from a collection while preserving the rest. For more advanced data handling techniques, see complex filters in Convex.
Merging Objects with the Spread Operator
The spread operator lets you combine properties from different objects into a new one.
const baseConfig = { timeout: 3000, retries: 3 };
const customConfig = { retries: 5, debug: true };
const finalConfig = { ...baseConfig, ...customConfig };
console.log(finalConfig); // { timeout: 3000, retries: 5, debug: true }
Notice that customConfig.retries overwrites baseConfig.retries. Properties from objects on the right override those from objects on the left.
Updating Properties in Objects
The spread operator excels at creating new objects with updated properties:
const userProfile = {
id: 'user_123',
name: 'Alice Chen',
role: 'developer'
};
const promotedUser = { ...userProfile, role: 'senior developer' };
console.log(promotedUser);
// { id: 'user_123', name: 'Alice Chen', role: 'senior developer' }
This immutable update pattern is common when working with typescript object type definitions in typed applications.
Cloning Objects
You can clone objects with the spread operator to create a shallow copy that functions independently from the original.
const apiResponse = { statusCode: 200, data: [] };
const cachedResponse = { ...apiResponse };
console.log(cachedResponse); // { statusCode: 200, data: [] }
When working with Convex data models, this technique helps create modified versions of documents before submitting updates. See types cookbook for more on handling TypeScript types with Convex.
Property Ordering and Override Patterns
Understanding how property position affects the final object is key to using the spread operator effectively.
const defaultSettings = {
theme: 'light',
notifications: true,
language: 'en'
};
// Override defaults with user preferences
const userPreferences = { theme: 'dark' };
const settings = { ...defaultSettings, ...userPreferences };
console.log(settings.theme); // 'dark'
// You can also override inline
const customSettings = {
...defaultSettings,
theme: 'dark',
notifications: false
};
console.log(customSettings);
// { theme: 'dark', notifications: false, language: 'en' }
This pattern is particularly useful for configuration objects. You can set sensible defaults, then let specific values override them. Just remember: last one wins.
When you need to merge configuration objects in TypeScript best practices, this right-to-left override behavior becomes a feature, not a bug.
Combining Multiple Objects with the Spread Operator
You can spread as many objects as you need into a single object.
const apiDefaults = { timeout: 5000, retry: true };
const envConfig = { baseUrl: 'https://api.example.com', retry: false };
const userOverrides = { timeout: 10000, debug: true };
const finalApiConfig = {
...apiDefaults,
...envConfig,
...userOverrides
};
console.log(finalApiConfig);
// { timeout: 10000, retry: false, baseUrl: 'https://api.example.com', debug: true }
The rightmost object always wins for duplicate keys. In this example, userOverrides.timeout overwrites apiDefaults.timeout, and envConfig.retry overwrites apiDefaults.retry.
Spread vs. Rest Parameters: What's the Difference?
This is where developers get confused. The same ... syntax does opposite things depending on context.
Spread Operator: Expands Values
The spread operator expands an array or object into individual elements.
const apiErrors = [400, 401, 403];
console.log(Math.max(...apiErrors)); // Expands to: Math.max(400, 401, 403)
Rest Parameters: Collects Values
Rest parameters collect multiple values into an array.
function logErrors(...errorCodes: number[]) {
// errorCodes is an array of all arguments passed
errorCodes.forEach(code => console.log(`Error ${code}`));
}
logErrors(400, 401, 403); // Arguments are collected into [400, 401, 403]
Quick rule: Spread scatters values out. Rest gathers values in.
You'll use spread when calling functions or building arrays/objects. You'll use rest parameters when defining functions that accept variable arguments.
Type Safety Challenges with Spread Operator
TypeScript's type system can get picky with the spread operator. Let's look at the most common issues and how to solve them.
The "Spread Argument Must Have Tuple Type" Error
You'll hit this error when spreading an array into a function that expects specific parameters:
function createEndpoint(method: string, path: string, version: number) {
return `${method} /${version}/${path}`;
}
const args = ['GET', 'users', 1];
createEndpoint(...args); // Error: A spread argument must either have a tuple type...
TypeScript sees args as a general string[] or any[] array, not as exactly three values. Here are three solutions:
Solution 1: Use a Tuple Type
const args: [string, string, number] = ['GET', 'users', 1];
createEndpoint(...args); // Works
Explicitly typing args as a tuple tells TypeScript exactly how many elements to expect and their types.
Solution 2: Use as const Assertion
const args = ['GET', 'users', 1] as const;
createEndpoint(...args); // Works
The as const assertion makes TypeScript infer the narrowest possible type. It treats args as a readonly tuple readonly ["GET", "users", 1] rather than a mutable array. This is often the cleanest solution. Learn more about const assertions in the readonly utility types guide.
Solution 3: Change Function to Accept Rest Parameters
function createEndpoint(...parts: [string, string, number]) {
const [method, path, version] = parts;
return `${method} /${version}/${path}`;
}
const args = ['GET', 'users', 1];
createEndpoint(...args); // Works
This makes the function signature match how you're calling it.
Type Widening with Object Spreads
When you spread objects, TypeScript doesn't always preserve the exact types you expect:
const strictConfig = { port: 3000 }; // Type: { port: number }
const config = { ...strictConfig, host: 'localhost' };
// TypeScript infers: { port: number; host: string }
// But won't catch typos like:
const badConfig = { ...strictConfig, hsot: 'localhost' }; // No error! 😱
TypeScript's structural typing means extra properties don't cause errors. If this matters for your use case, consider using exact types or validation libraries.
Handling Nested Objects with the Spread Operator
Be careful with nested objects. The spread operator only creates a shallow copy.
const apiConfig = {
endpoint: '/api',
headers: { 'Content-Type': 'application/json' }
};
const modifiedConfig = { ...apiConfig };
modifiedConfig.headers['Authorization'] = 'Bearer token';
console.log(apiConfig.headers['Authorization']); // 'Bearer token' 😱
Both objects reference the same headers object. Changing one affects the other.
To create a true deep copy, you need to spread at every nested level:
const apiConfig = {
endpoint: '/api',
headers: { 'Content-Type': 'application/json' }
};
const modifiedConfig = {
...apiConfig,
headers: { ...apiConfig.headers } // Spread the nested object too
};
modifiedConfig.headers['Authorization'] = 'Bearer token';
console.log(apiConfig.headers['Authorization']); // undefined ✅
console.log(modifiedConfig.headers['Authorization']); // 'Bearer token' ✅
This deep cloning approach is essential when working with complex utility types in applications where data immutability matters.
For deeply nested structures, consider using libraries like structuredClone() (modern browsers/Node 17+) or Immer for more maintainable code.
Using the Spread Operator in Function Arguments
The spread operator can expand arrays into function arguments:
function calculateTotal(price: number, tax: number, shipping: number): number {
return price + tax + shipping;
}
const costs = [100, 8.5, 12];
const total = calculateTotal(...costs); // Expands to calculateTotal(100, 8.5, 12)
console.log(total); // 120.5
This is especially useful with functions that accept variable-length argument lists:
function logApiCalls(...endpoints: string[]): void {
endpoints.forEach(endpoint => {
console.log(`Calling: ${endpoint}`);
});
}
logApiCalls('/users', '/posts', '/comments');
// Logs each endpoint
When building Convex functions, this pattern can help create flexible APIs. Check out custom functions to learn how to build adaptable function interfaces in Convex.
Combining Multiple Arrays
Need to merge several arrays? The spread operator makes it clean:
const errorCodes = [400, 401];
const serverErrors = [500, 502];
const timeoutErrors = [408, 504];
const allErrors = [...errorCodes, ...serverErrors, ...timeoutErrors];
console.log(allErrors); // [400, 401, 500, 502, 408, 504]
You can mix direct values and spread arrays:
const httpMethods = ['POST', 'PUT', 'PATCH'];
const allMethods = ['GET', ...httpMethods, 'DELETE'];
console.log(allMethods); // ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
This pattern works well with map operations when processing collections of data.
Performance Considerations
For most use cases, the spread operator's performance is fine. But there are scenarios where it can slow you down.
When Performance Matters
The spread operator creates a new array or object every time it runs. This means:
1. Avoid Spread in Loops
// Poor performance: creates a new array on every iteration
let results = [];
for (let i = 0; i < 1000; i++) {
results = [...results, processItem(i)]; // O(n²) time complexity
}
// Better: use push() for building arrays in loops
let results = [];
for (let i = 0; i < 1000; i++) {
results.push(processItem(i)); // O(n) time complexity
}
2. Be Cautious with Large Datasets
// Slow with large arrays
const hugeArray = Array(100000).fill(0);
const combined = [...hugeArray, ...hugeArray]; // Copies 200,000 items
// Faster for large arrays
const combined = hugeArray.concat(hugeArray);
Array methods like concat() are typically more performant at scale because they're optimized in the JavaScript engine.
When Spread Operator is Fine
For small to medium-sized arrays (under a few thousand elements) and objects, the readability benefits outweigh the minor performance cost. Don't prematurely optimize.
Use spread when:
- Working with configuration objects
- Building UI state updates
- Merging API responses
- Creating shallow copies for immutability
Consider alternatives when:
- Inside tight loops
- Processing large datasets (10k+ items)
- Building arrays incrementally
- Performance profiling shows it's a bottleneck
Destructuring Assignments using the Spread Operator
The spread operator works beautifully in destructuring for both arrays and objects.
Array Destructuring
const [primary, secondary, ...fallbackServers] = [
'us-east-1',
'us-west-2',
'eu-west-1',
'ap-south-1',
'sa-east-1'
];
console.log(primary); // 'us-east-1'
console.log(secondary); // 'us-west-2'
console.log(fallbackServers); // ['eu-west-1', 'ap-south-1', 'sa-east-1']
Object Destructuring
const apiResponse = {
userId: 'user_123',
email: 'alice@example.com',
createdAt: '2025-01-15',
role: 'admin'
};
const { userId, ...userMetadata } = apiResponse;
console.log(userId); // 'user_123'
console.log(userMetadata);
// { email: 'alice@example.com', createdAt: '2025-01-15', role: 'admin' }
This pattern is valuable when working with Partial<T> types to extract specific properties while preserving others.
When building TypeScript applications, destructuring can help manage complex document structures. Learn more in functional relationships helpers.
Key Takeaways
The TypeScript spread operator simplifies working with arrays and objects, but you need to understand its type system quirks and performance characteristics.
Remember:
- Spread creates shallow copies only. For nested structures, spread at every level or use deep cloning utilities.
- Use tuple types or
as constassertions when spreading into function arguments to avoid type errors. - Property order matters when spreading objects. Rightmost properties win.
- Spread and rest parameters use the same syntax but do opposite things. Spread expands, rest collects.
- Avoid spread in tight loops or with large datasets. Use
push(),concat(), or immutability libraries instead. - For most everyday use cases, the readability gains make spread the right choice.
The spread operator is one of TypeScript's most useful features once you know how to work with its type system. When you understand how type inference works and when to add explicit types, you can write cleaner, more maintainable code.
For more TypeScript tips and tricks specific to Convex, check out TypeScript best practices.