The TypeScript spread operator is three dots (…) written before an array, an object or another iterable value, and it expands that value into its individual elements or properties. It is the JavaScript spread syntax with type checking. The same three dots in a parameter list or on the left side of an assignment do the opposite, because they collect values into a rest parameter or a rest variable.
We use the spread operator to copy and merge arrays and objects without changing the originals, and to pass the elements of an array as separate function arguments. For example, a settings screen merges the settings a user saved over the app defaults in one line.
The following example shows six common forms of spread and rest, compiled with TypeScript 7.0.2 and run on Node.js 22, with the value of each line as a comment.
const fruits = ["apple", "banana"];
const lokesh = { name: "Lokesh", age: 37 };
// 1. Copy and extend an array
const copy = [...fruits]; // copy = ["apple", "banana"]
const more = [...fruits, "cherry"]; // more = ["apple", "banana", "cherry"]
// 2. Copy an object and override a property
const older = { ...lokesh, age: 38 }; // older = { name: "Lokesh", age: 38 }
// 3. Spread an array into function arguments
const max = Math.max(...[3, 7, 5]); // max = 7
// 4. Rest parameter collects arguments
const sum = (...nums: number[]) => nums.reduce((a, b) => a + b, 0);
const total = sum(1, 2, 3); // total = 6
// 5. Rest destructuring removes a property
const { age, ...rest } = lokesh; // rest = { name: "Lokesh" }
// 6. Strings and Sets are iterable
const letters = [..."hi"]; // letters = ["h", "i"]
const unique = [...new Set([1, 1, 2])]; // unique = [1, 2]
Notice that every spread creates a new array or object, so fruits and lokesh stay unchanged.
Next, we look at spreading arrays and objects, the shallow-copy limit, function arguments, rest parameters, strings, Sets and Maps, and tuple types. Along the way, we see the compile errors TypeScript reports for spread code and two cases where the compiler cannot help.
1. Spreading Arrays
Inside square brackets, …fruits writes out every element of fruits at that position, and the result is always a new array. So […fruits] is a copy, and […a, …b] merges two arrays into a third one. New items can go anywhere in the literal, before or after a spread part.
const fruits = ["apple", "banana"];
const veggies = ["carrot"];
// 1. Copy
const copy = [...fruits]; // copy = ["apple", "banana"]
const isNew = copy !== fruits; // isNew = true
// 2. Merge and add items anywhere
const all = [...fruits, ...veggies]; // all = ["apple", "banana", "carrot"]
const withFirst = ["mango", ...fruits]; // withFirst = ["mango", "apple", "banana"]
// 3. Push many items at once
copy.push(...veggies); // copy = ["apple", "banana", "carrot"]
// 4. Reverse a copy, not the original
const reversed = [...fruits].reverse(); // reversed = ["banana", "apple"]
const same = fruits.toReversed(); // same = ["banana", "apple"]
Copying before a mutating method such as reverse() or sort() keeps the original array unchanged. Keeping the original matters when the array is state in a UI framework or is shared with other code. For example, a product list page sorts the products by price for display, while the cart code still reads the original order.
Since ES2023, toReversed(), toSorted() and toSpliced() return a changed copy, so we no longer need the spread step for those three methods. Merging with spread gives the same result as concat(), and we prefer spread because it also places single items in the same expression. We can find more ways in adding items to an array.
2. Spreading Objects
Inside curly braces, …lokesh copies the object’s own enumerable properties into the new object. When the same property name appears more than once, the last value overwrites the earlier ones. Because of the last-value rule, spread is the standard way to create an updated copy of an object or to merge user settings over defaults.
const lokesh = { name: "Lokesh", age: 37 };
// 1. Copy and add a property
const withCity = { ...lokesh, city: "Delhi" }; // { name: "Lokesh", age: 37, city: "Delhi" }
// 2. Later properties win
const older = { ...lokesh, age: 38 }; // older.age = 38
// 3. Merge settings over defaults
const defaults = { theme: "light", size: 12 };
const saved = { size: 14 };
const settings = { ...defaults, ...saved }; // settings = { theme: "light", size: 14 }
// 4. Add a property only when a condition is true
const roles = ["admin", "editor"];
const isAdmin = roles.includes("admin"); // isAdmin = true
const user = { ...lokesh, ...(isAdmin && { role: "admin" }) }; // user.role = "admin"
// 5. Remove a property with rest
const { age, ...withoutAge } = lokesh; // withoutAge = { name: "Lokesh" }
Step 4 is the conditional spread pattern. When the condition is false, the expression spreads false, which adds nothing. TypeScript types the result as { name: string; age: number; role?: string | undefined }, so the code that reads role must handle a missing value. Step 5 uses rest destructuring, where age gets one property and withoutAge gets all the others, without changing lokesh.
TypeScript checks the properties written next to a spread. With a declared target type, the compiler catches typos and wrong value types, and it reports a property that the spread will always overwrite.
interface User { name: string; age: number }
const lokesh: User = { name: "Lokesh", age: 37 };
const typo: User = { ...lokesh, agee: 38 };
const wrong: User = { ...lokesh, age: "38" };
const lost = { age: 0, ...lokesh };
src/err.ts(4,33): error TS2561: Object literal may only specify known properties, but 'agee' does not exist in type 'User'. Did you mean to write 'age'?
src/err.ts(5,34): error TS2322: Type 'string' is not assignable to type 'number'.
src/err.ts(6,16): error TS2783: 'age' is specified more than once, so this usage will be overwritten.
The last error is about order. The object lokesh always has an age, so the earlier age: 0 can never survive. To set a default, we put the default first and the spread of a type with optional properties after it, as defaults and saved do in step 3 of the previous snippet.
3. Spread Makes a Shallow Copy
Spread copies only the first level. A nested object or array is not copied, and the new object holds a reference to the same nested object as the original. So changing a nested value through the copy changes the original too.
A typical case is a customer profile form that copies the profile with spread. When the user edits the city in the form, the shared address object of the saved profile changes too, before the user clicks Save.
const lokesh = { name: "Lokesh", address: { city: "Delhi" } };
// 1. Spread copies only the top level
const copy = { ...lokesh };
copy.address.city = "Pune";
const original = lokesh.address.city; // original = "Pune", changed too
// 2. Copy the nested object as well
const copy2 = { ...lokesh, address: { ...lokesh.address } };
copy2.address.city = "Mumbai";
const kept = lokesh.address.city; // kept = "Pune"
// 3. Deep copy
const deep = structuredClone(lokesh);
deep.address.city = "Chennai";
const still = lokesh.address.city; // still = "Pune"
Spreading each nested level, as in step 2, is the common approach for state updates, because it copies only the part that changes. structuredClone() copies every level and is available globally in Node.js 17+ and all current browsers. It cannot copy functions, and class instances come back as plain objects.
Class instances lose more than their class with spread. Methods are defined on the class prototype, not on the object itself, so the spread copies only the fields. TypeScript shows the missing method in the type of the copy.
class Person {
name = "Raj";
greet() { return "Hi " + this.name; }
}
const plain = { ...new Person() }; // plain = { name: "Raj" }
plain.greet();
src/err.ts(6,7): error TS2339: Property 'greet' does not exist on type '{ name: string; }'.
4. Spreading Arrays Into Function Arguments
In a function call, …scores passes each element as a separate argument. It replaces the older apply() method, which took the arguments as an array. The methods Math.max() and Math.min() are the classic examples, because they accept any number of arguments but not an array.
For example, a weather dashboard gets the hourly temperatures of a day as an array and shows the highest one with Math.max(…temperatures).
const scores = [3, 7, 5];
// 1. Spread an array into separate arguments
const max = Math.max(...scores); // max = 7
// 2. A tuple matches fixed parameters
const multiply = (a: number, b: number) => a * b;
const pair: [number, number] = [4, 5];
const product = multiply(...pair); // product = 20
// 3. The older way with apply()
const oldMax = Math.max.apply(null, scores); // oldMax = 7
For a function with a fixed number of parameters, TypeScript must know how many elements the array has. The type number[] can hold any count, so multiply(…scores) does not compile.
src/err.ts(3,26): error TS2556: A spread argument must either have a tuple type or be passed to a rest parameter.
The fix is either a tuple type, as pair shows, or [4, 5] as const, which makes the compiler treat the array literal as a fixed-length tuple. The method Math.max() accepts the plain array because its parameter is a rest parameter.
5. Rest Parameters and Rest Destructuring
Rest uses the same three dots in the opposite direction. In a parameter list, …nums collects all remaining arguments into one array. In destructuring, it collects the remaining elements or properties into a new array or object. The rest element must always come last.
// 1. Rest parameter: any number of arguments
const sum = (...nums: number[]) => nums.reduce((a, b) => a + b, 0);
const s0 = sum(); // s0 = 0
const s3 = sum(1, 2, 3); // s3 = 6
// 2. Rest in array destructuring
const [first, ...others] = ["apple", "banana", "cherry"];
// first = "apple", others = ["banana", "cherry"]
// 3. Rest in object destructuring
const { name, ...details } = { name: "Lokesh", age: 37, city: "Delhi" };
// name = "Lokesh", details = { age: 37, city: "Delhi" }
The position of the three dots tells spread and rest apart.
- Three dots on the receiving side (parameters, the left side of =) mean rest.
- Three dots on the giving side (arguments, array and object literals) mean spread.
The type of a rest parameter is always an array or tuple type, such as number[]. A logging helper such as log(level, …messages) is a common real case, because callers pass one message or several. We cover the signatures in more detail in rest, optional and default parameters.
6. Spreading Strings, Set and Map
Array spread works with any iterable value, which is a value that we can loop over with for…of. Strings are iterable, and so are Set and Map, so spread turns them into arrays. We use spread with them to remove duplicates and to list or merge Map entries.
// 1. String to characters
const letters = [..."hello"]; // letters = ["h", "e", "l", "l", "o"]
// 2. Remove duplicates with a Set
const unique = [...new Set([1, 2, 2, 3])]; // unique = [1, 2, 3]
// 3. Map entries and keys
const ages = new Map([["Lokesh", 37], ["Raj", 35]]);
const pairs = [...ages]; // pairs = [["Lokesh", 37], ["Raj", 35]]
const names = [...ages.keys()]; // names = ["Lokesh", "Raj"]
// 4. Merge two Maps
const more = new Map([["John", 40]]);
const merged = new Map([...ages, ...more]); // merged.size = 3
// 5. Object spread of a Map copies nothing
const empty = { ...ages }; // empty = {}
A plain object is not iterable, so […{ name: “Lokesh” }] fails with error TS2488, which says that the type { name: string; } must have a [Symbol.iterator]() method that returns an iterator. The same error appears when an array might be undefined, for example a variable of type number[] | undefined.
Step 5 is a case where the compiler does not help. A Map stores its entries internally, not as object properties, so object spread produces an empty object. TypeScript still gives empty a type with all the Map methods, so empty.get(“Lokesh”) compiles and fails at runtime with “TypeError: empty.get is not a function”. To copy a Map, we write new Map(ages), and the articles on TypeScript Map and TypeScript Set show more conversions.
7. Spread With Tuple Types
When we spread a tuple into an array literal, TypeScript infers a plain array type by default and loses the positions. A tuple type annotation or as const keeps them. Spread also works inside type definitions, where it builds tuple types from other tuple types. These types are called variadic tuple types and were added in TypeScript 4.0.
const pair: [string, number] = ["Lokesh", 37];
// 1. Without a type, the result is an array
const loose = [...pair, true]; // type (string | number | boolean)[]
// 2. as const or a tuple type keeps the positions
const fixed = [...pair, true] as const; // type readonly [string, number, true]
const typed: [string, number, boolean] = [...pair, true];
// 3. Spread inside a tuple type
type Named<T extends unknown[]> = [string, ...T];
const row: Named<[number, boolean]> = ["Lokesh", 37, true];
All four variables hold the same runtime value, [“Lokesh”, 37, true], and only the types differ. With loose, TypeScript cannot tell that the first element is a string, so loose[0].toUpperCase() does not compile, whereas with typed it compiles.
8. Building and Running the Examples
The spread-operator project on GitHub contains every snippet that compiles, one file per section, and logs the value behind each comment. Any Node.js release from 22 on can run it after npm install adds TypeScript 7.0.2.
npm install
npm start
9. Conclusion
The TypeScript spread operator expands arrays, objects and other iterables into new arrays, new objects and function arguments, and the rest syntax collects values back into one variable.
Copies made with spread are shallow, and class methods are not copied. When a property name repeats, the last value overwrites the earlier ones. TypeScript checks the properties written next to a spread and requires tuple types for fixed parameters. It does not warn when we apply object spread to a Map or Set.
10. References
MDN describes the runtime rules for spread and rest, and the TypeScript Handbook covers how the results are typed.
- MDN: Spread syntax (…)
- MDN: Rest parameters
- MDN: Destructuring
- MDN: structuredClone()
- TypeScript Handbook: Object types
- TypeScript 4.0 release notes: variadic tuple types
Happy Learning !!