# JavaScript immutable array methods: toSorted(), toReversed(), toSpliced(), with()

> Need to sort or reverse an array without mutating it? Use toSorted(), toReversed(), toSpliced(), and with() to get a new copy instead.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-08-21 | Topics: [JavaScript](https://flaviocopes.com/tags/js/) | Canonical: https://flaviocopes.com/javascript-immutable-array-methods/

JavaScript has four array methods that return a changed copy:

- `toSorted()` sorts a copy
- `toReversed()` reverses a copy
- `toSpliced()` inserts, removes, or replaces items in a copy
- `with()` 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](https://flaviocopes.com/courses/javascript/) covers arrays, callbacks, and transformation methods first.

## The problem with mutation

Suppose you receive a list of scores and sort it for a leaderboard:

```js
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:

```js
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:

```js
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()`:

```js
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:

```js
console.log(scores === leaderboard) // false
```

The comparator works exactly as it does with `sort()`. For ascending numbers:

```js
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:

```js
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](https://flaviocopes.com/javascript-sort-comparators/) for more examples.

You can sort objects by a property:

```js
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:

```js
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:

```js
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:

```text
array.toSpliced(start, deleteCount, ...items)
```

Replace one item:

```js
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:

```js
const tasks = ['write', 'ship']
const updated = tasks.toSpliced(1, 0, 'test')

console.log(updated) // ['write', 'test', 'ship']
```

Remove without inserting:

```js
const tasks = ['write', 'review', 'ship']
const updated = tasks.toSpliced(1, 1)

console.log(updated) // ['write', 'ship']
```

Negative indexes count from the end:

```js
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`:

```js
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](https://flaviocopes.com/how-to-remove-item-from-array/).

## Replace one item with with()

Use `with()` when you already know the index:

```js
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:

```js
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`:

```js
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:

```js
const sorted = [...items].sort(compareItems)
```

with:

```js
const sorted = items.toSorted(compareItems)
```

Replace copy-and-reverse:

```js
const reversed = [...items].reverse()
```

with:

```js
const reversed = items.toReversed()
```

Replace a copied `splice()` update:

```js
const updated = [...items]
updated.splice(index, 1, replacement)
```

with:

```js
const updated = items.toSpliced(index, 1, replacement)
```

And replace copied index assignment:

```js
const updated = [...items]
updated[index] = replacement
```

with:

```js
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:

```js
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:

```js
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:

```js
const products = [
  { id: 1, name: 'Keyboard', price: 90, stock: 3 },
  { id: 2, name: 'Mouse', price: 40, stock: 8 },
]
```

Find the product:

```js
const index = products.findIndex(product => product.id === 2)
```

Create the replacement object:

```js
const changedProduct = {
  ...products[index],
  stock: products[index].stock - 1,
}
```

Then replace it in a copied array:

```js
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.

```js
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:

```js
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()`:

```js
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`:

```js
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:

```js
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:

```js
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:

```js
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](https://flaviocopes.com/javascript-array/).
