Three dots (…) written before an array, an object or another iterable value make up the spread operator: they expand that value into its individual elements or properties. The TypeScript spread operator is the JavaScript spread syntax with type checking. We use it to copy and merge arrays and objects without changing the originals, and to pass the elements of an array as separate function arguments. The same three dots in a parameter list or on the left side of an assignment do the opposite job: they collect values into a rest parameter or a rest variable.
The examples show spreading arrays and objects, the shallow-copy limit, function arguments, rest parameters, strings, Sets and Maps, and tuple types. They also show the compile errors TypeScript reports for spread code, including two cases where the compiler cannot help. All snippets ran on Node.js 22 after compiling with TypeScript 7.0.2.
Six forms cover nearly every use of the three dots in application code. Each comment holds the value its line produced.
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]
1. Spreading Arrays
Inside square brackets, …fruits writes out every element of fruits at that position. 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 before, between or after the spread parts.
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, which matters when the array is state in a UI framework or is shared with other code. Since ES2023, toReversed(), toSorted() and toSpliced() return a changed copy directly, so the spread step is no longer needed for those three. Merging with spread gives the same result as concat(); we prefer spread because it also places single items in the same expression. More ways to add items are 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 one wins. That rule makes spread 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: age receives one property and withoutAge receives all the others, without changing lokesh.
TypeScript checks the properties written next to a spread. With a declared target type, it 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. lokesh always has an age, so the earlier age: 0 can never survive. To set a default, the default goes first and the spread of a type with optional properties goes after it, as defaults and saved do above.
3. Spread Makes a Shallow Copy
Spread copies only the first level. A nested object or array is not copied; the new object holds a reference to the same nested object as the original. Changing a nested value through the copy therefore changes the original too.
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 usual 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 that with spread. Methods live on the class prototype, not on the object itself, so the spread copies only the fields. TypeScript reflects this 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. This replaces the older apply() method, which took the arguments as an array. Math.max() and Math.min() are the classic examples, because they accept any number of arguments but not an array.
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 a tuple type, as pair shows, or [4, 5] as const, which makes the compiler treat the array literal as a fixed-length tuple. 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" }
A simple rule tells them 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[]. Rest, optional and default parameters covers rest parameters in function signatures in more detail.
6. Spreading Strings, Set and Map
Array spread works with any iterable value, which is a value that can be looped over with for…of. Strings, Set and Map are iterable, so spread turns them into arrays. This gives short idioms for splitting a string into characters, removing duplicates, and listing or merging 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, “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 then fails at runtime with “TypeError: empty.get is not a function”. To copy a Map, we write new Map(ages); 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 forgets 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 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]. Only the types differ. With loose, TypeScript cannot tell that the first element is a string, so loose[0].toUpperCase() does not compile; with typed, it does.
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 a new array, a new object or a list of function arguments, and the rest syntax collects values back into one variable. Copies made with spread are shallow, the last property wins when names repeat, and class methods are not copied. TypeScript checks the properties written next to a spread, requires tuple types for fixed parameters, and rejects non-iterable values in array spread, but it does not warn when object spread is applied to a Map or Set.
10. References
MDN describes the runtime rules for spread and rest; 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 !!