JavaScript immutable array methods: toSorted(), toReversed(), toSpliced(), with()
By Flavio Copes
Need to sort or reverse an array without mutating it? Use toSorted(), toReversed(), toSpliced(), and with() to get a new copy instead.
JavaScript has four array methods that return a changed copy:
toSorted()sorts a copytoReversed()reverses a copytoSpliced()inserts, removes, or replaces items in a copywith()replaces one item in a copy
The original array stays unchanged.
These methods solve a common source of bugs. The older sort(), reverse(), and splice() methods modify the array you call them on.
If array methods are new to you, the free JavaScript course covers arrays, callbacks, and transformation methods first.
The problem with mutation
Suppose you receive a list of scores and sort it for a leaderboard:
const scores = [88, 42, 95, 71]
const leaderboard = scores.sort((a, b) => b - a)
console.log(scores) // [95, 88, 71, 42]
console.log(leaderboard) // [95, 88, 71, 42]
The scores array changed too. Both variables point to the same array:
console.log(scores === leaderboard) // true
That is fine when you intend to reuse the sorted array. It is a bug when another part of the program expects the original order.
The older workaround was to copy first:
const leaderboard = [...scores].sort((a, b) => b - a)
That works. The newer methods express the intention directly.
Sort a copy with toSorted()
Use toSorted() like sort():
const scores = [88, 42, 95, 71]
const leaderboard = scores.toSorted((a, b) => b - a)
console.log(scores) // [88, 42, 95, 71]
console.log(leaderboard) // [95, 88, 71, 42]
The result is a different array:
console.log(scores === leaderboard) // false
The comparator works exactly as it does with sort(). For ascending numbers:
const prices = [19, 5, 12]
const cheapestFirst = prices.toSorted((a, b) => a - b)
Do not omit the comparator for numbers. The default comparison converts values to strings:
const prices = [2, 11, 3]
console.log(prices.toSorted()) // [11, 2, 3]
console.log(prices.toSorted((a, b) => a - b)) // [2, 3, 11]
Read how sort comparators work for more examples.
You can sort objects by a property:
const products = [
{ name: 'Keyboard', price: 90 },
{ name: 'Mouse', price: 40 },
{ name: 'Display', price: 300 },
]
const byPrice = products.toSorted((a, b) => a.price - b.price)
products keeps its original order. byPrice contains the same objects in a new array.
Reverse a copy with toReversed()
toReversed() returns the items in reverse order:
const months = ['January', 'February', 'March']
const latestFirst = months.toReversed()
console.log(months)
// ['January', 'February', 'March']
console.log(latestFirst)
// ['March', 'February', 'January']
The old approach was:
const latestFirst = [...months].reverse()
Use toReversed() when you want the copy and reverse() when you deliberately want to change the existing array.
Edit a copy with toSpliced()
toSpliced() is the copying version of splice().
Its arguments are:
array.toSpliced(start, deleteCount, ...items)
Replace one item:
const tasks = ['write', 'review', 'ship']
const updated = tasks.toSpliced(1, 1, 'test')
console.log(tasks) // ['write', 'review', 'ship']
console.log(updated) // ['write', 'test', 'ship']
Insert without removing:
const tasks = ['write', 'ship']
const updated = tasks.toSpliced(1, 0, 'test')
console.log(updated) // ['write', 'test', 'ship']
Remove without inserting:
const tasks = ['write', 'review', 'ship']
const updated = tasks.toSpliced(1, 1)
console.log(updated) // ['write', 'ship']
Negative indexes count from the end:
const tasks = ['write', 'review', 'test', 'ship']
const updated = tasks.toSpliced(-2, 1, 'deploy')
console.log(updated)
// ['write', 'review', 'deploy', 'ship']
The -2 points to test. One item is removed there, then deploy is inserted.
You can also remove everything from an index to the end by omitting deleteCount:
const tasks = ['write', 'review', 'test', 'ship']
const updated = tasks.toSpliced(2)
console.log(updated) // ['write', 'review']
Be careful with an explicit undefined second argument. It is converted to zero, so toSpliced(2, undefined) removes nothing. Omit the argument when you mean “remove the rest.”
Unlike splice(), toSpliced() returns the complete updated array. splice() returns only the removed items.
For other removal patterns, read how to remove an item from an array.
Replace one item with with()
Use with() when you already know the index:
const colors = ['red', 'green', 'blue']
const updated = colors.with(1, 'yellow')
console.log(colors) // ['red', 'green', 'blue']
console.log(updated) // ['red', 'yellow', 'blue']
Negative indexes count from the end:
const colors = ['red', 'green', 'blue']
const updated = colors.with(-1, 'purple')
console.log(updated) // ['red', 'green', 'purple']
An index outside the array throws a RangeError:
const colors = ['red', 'green', 'blue']
colors.with(5, 'purple') // RangeError
This is different from direct assignment, which can create empty positions beyond the current length.
Translate older code one operation at a time
The copying methods have a direct relationship with older mutating code.
To sort without changing the source, replace the copy-and-sort pattern:
const sorted = [...items].sort(compareItems)
with:
const sorted = items.toSorted(compareItems)
Replace copy-and-reverse:
const reversed = [...items].reverse()
with:
const reversed = items.toReversed()
Replace a copied splice() update:
const updated = [...items]
updated.splice(index, 1, replacement)
with:
const updated = items.toSpliced(index, 1, replacement)
And replace copied index assignment:
const updated = [...items]
updated[index] = replacement
with:
const updated = items.with(index, replacement)
The new forms keep the copy operation and the change in one expression. There is no temporary array that another line can accidentally mutate.
Do not replace mutation blindly. First check whether other code depends on the original array changing.
Updating state without mutation
Copying methods fit state updates well because the new array has a new identity.
For example, a React state update can replace one todo item:
setTodos(current => {
const index = current.findIndex(todo => todo.id === changed.id)
if (index === -1) return current
return current.with(index, changed)
})
Or move a newly added item to the front of a sorted list:
setTodos(current =>
[...current, newTodo].toSorted((a, b) => b.createdAt - a.createdAt)
)
The state library receives a new array and can detect the change without inspecting every item.
Build an immutable update from the item outward
When array items are objects, copy the changed object and then copy the array.
Start with a product list:
const products = [
{ id: 1, name: 'Keyboard', price: 90, stock: 3 },
{ id: 2, name: 'Mouse', price: 40, stock: 8 },
]
Find the product:
const index = products.findIndex(product => product.id === 2)
Create the replacement object:
const changedProduct = {
...products[index],
stock: products[index].stock - 1,
}
Then replace it in a copied array:
const updatedProducts = products.with(index, changedProduct)
There are two copies here for two separate layers:
- the object spread copies the changed product
with()copies the array
The other product object is shared because it did not change. This is the normal immutable update pattern for nested data.
The copy is shallow
These methods copy the array, not the objects inside it.
const products = [
{ name: 'Keyboard', price: 90 },
{ name: 'Mouse', price: 40 },
]
const sorted = products.toSorted((a, b) => a.price - b.price)
sorted[0].price = 35
console.log(products[1].price) // 35
Both arrays still reference the same product objects. If you need to change an object without affecting the original, copy that object too.
Chaining creates more than one copy
Copying methods compose nicely:
const visibleProducts = products
.filter(product => product.stock > 0)
.toSorted((a, b) => a.price - b.price)
.toReversed()
This is readable, but every copying method creates another array.
For a small UI list, that cost is usually irrelevant. For a very large list in a hot path, avoid transformations you do not need. In this example, changing the comparator could remove toReversed():
const visibleProducts = products
.filter(product => product.stock > 0)
.toSorted((a, b) => b.price - a.price)
Immutability does not mean we should create copies without thinking. It means changes are explicit and the source value stays stable.
Common mistakes
The first mistake is assuming a copying method clones nested data. It only creates a new array.
The second is forgetting the numeric comparator with toSorted(). The default sort compares string representations.
The third is using with() with an index that might be -1:
const index = products.findIndex(product => product.id === 99)
const updated = products.with(index, replacement)
-1 is a valid relative index, so this replaces the last item instead of reporting “not found.”
Check first:
const updated =
index === -1
? products
: products.with(index, replacement)
This is an easy bug to miss because with(-1, value) is valid by design.
The fourth mistake is expecting copying methods to make mutation impossible. The returned array is still mutable:
const sorted = scores.toSorted((a, b) => b - a)
sorted.push(100)
The source stays unchanged, but sorted changes. These methods copy; they do not freeze.
When mutation is still fine
Copying has a cost. A new array must be allocated and filled.
Mutation is reasonable when the array is local, temporary, and owned by one function:
function rankScores(input) {
const scores = Array.from(input)
scores.sort((a, b) => b - a)
return scores
}
No other code can observe the local scores mutation.
My advice is to use the copying methods at boundaries: function arguments, shared data, UI state, and cached results. Use mutation inside a small private scope when it makes the code clearer or avoids unnecessary copies.
Pick the method from the change
Use toSorted() when only the order changes.
Use toReversed() when the order must flip.
Use toSpliced() when the array length can change, or when you need to insert or remove around an index.
Use with() when the length stays the same and exactly one known position changes.
That last distinction is useful: with() replaces, while toSpliced() can reshape.
These methods work in current browsers and modern Node.js releases. Check compatibility if your project still supports an older runtime. You can find the other methods in the JavaScript array guide.
Related posts about js: