toSorted, toReversed, with(): JS's New Array Methods
toSorted(), toReversed(), toSpliced(), and with() copy an array instead of mutating it, fixing a long-standing footgun in JavaScript's Array API.
JavaScript’s Array.prototype now has copying counterparts for its most commonly mutated methods: toSorted(), toReversed(), toSpliced(), and with(). Each one returns a new array and leaves the original untouched, which is exactly what sort(), reverse(), splice(), and direct index assignment have never done.
This closes a gap that’s tripped up JavaScript developers for years. [1, 3, 2].sort() doesn’t just return a sorted array — it sorts the array in place and returns a reference to that same, now-mutated array. If another part of your code was holding a reference to it, or if it was a prop passed down to a component, that mutation happens underneath it. Our piece on Array.sort() pitfalls covers the comparator side of that footgun; the new methods address the mutation side.
The four methods
| Mutating method | Copying method | What it does |
|---|---|---|
sort() | toSorted() | Sorts the array |
reverse() | toReversed() | Reverses element order |
splice() | toSpliced() | Removes/inserts elements at an index |
arr[i] = x | with(i, x) | Replaces the element at index i |
Each copying method takes the same arguments as its mutating counterpart (toSorted() accepts a compare function, toSpliced() accepts start/deleteCount/items) but returns a fresh array instead of touching the receiver.
const scores = [40, 10, 25];
const ranked = scores.toSorted((a, b) => b - a);
console.log(ranked); // [40, 25, 10]
console.log(scores); // [40, 10, 25] — unchanged
const first = ["a", "b", "c"];
const updated = first.with(1, "z");
console.log(updated); // ["a", "z", "c"]
console.log(first); // ["a", "b", "c"] — unchanged
with() is the one without an obvious mutating “partner” — it replaces the ad hoc pattern of copying an array just to change one index:
// Before
const next = [...items];
next[i] = value;
// After
const next = items.with(i, value);
Why this matters for state management
Frameworks built around immutable state — React, Redux, most signal-based systems covered in our piece on signals for frontend state — rely on reference changes to detect updates. Mutate an array in place and a === comparison won’t notice; the reference didn’t change, so a component skips re-rendering even though the data underneath it is different.
Before these methods existed, avoiding that meant the spread operator plus a mutating call, or writing your own copy helper:
// The old workaround
const sorted = [...items].sort((a, b) => a.date - b.date);
That works, but it’s easy to forget the spread, and it’s an extra allocation either way. items.toSorted(...) says exactly what it does and can’t be misused into mutating the original by accident.
with() vs shallow copy and reassignment
with() specifically replaces one element. It’s worth being precise about what “copy” means here: like the spread operator, these methods perform a shallow copy — the array itself is new, but if an element is an object, both the original and the copy still point to the same object. Replacing an object at an index with with() doesn’t clone that object; it just swaps which object sits at that position.
const users = [{ name: "Ana" }, { name: "Bo" }];
const next = users.with(0, { ...users[0], name: "Anastasia" });
// users[0] and next[0] are different objects — this is a real update
If you mutate users[0].name directly instead, both arrays would “see” the change, because they share the same object reference. with() only helps if you also treat the replacement value as a new object.
How this fits with functional array methods
map(), filter(), and reduce() — covered in map, filter, reduce explained — have always been non-mutating, which is part of why they’re the default choice for transforming arrays in functional style. toSorted(), toReversed(), toSpliced(), and with() extend that same non-mutating contract to the handful of operations that didn’t have a non-mutating option before.
That means a simple rule now covers the whole array API: if you want to transform data without touching the original, there’s a method for it. You reach for the mutating versions only when you specifically want to change a value in place — inside a tight loop, or when working with an array you know nothing else references.
Browser and runtime support
These methods landed as part of the “Change Array by Copy” proposal and are available in current versions of major browsers, Node.js, and other JavaScript engines. As with any newer language feature, check your minimum supported runtime before relying on it in a library that ships to environments you don’t control — a bundler target or polyfill may be needed for older browsers.
A quick rule of thumb
The naming convention makes the choice easy to remember once it clicks: any method starting with to returns a new array, mirroring how toString() and toUpperCase() never mutate the value they’re called on. Anything without that prefix — sort(), reverse(), splice() — follows the array’s older, mutating convention. When in doubt about whether a method you’re calling touches the original array, that prefix is the tell.
Reaching for the mutating versions still makes sense in a few cases: building up an array inside a tight loop where allocating a new array on every iteration would be wasteful, or working with a local array you’ve just created and know nothing else references. Outside of those, defaulting to the to-prefixed methods removes an entire category of “why did this other part of the code change” bugs, at the cost of one extra array allocation per call — a tradeoff that’s almost always worth making for anything beyond a hot inner loop.
TypeScript support
TypeScript’s standard library type definitions include these methods as of a recent lib target, so no extra typings package is needed — just make sure your tsconfig.json’s lib setting includes a version that covers them, the same setting that governs access to other modern built-ins.
The takeaway
toSorted(), toReversed(), toSpliced(), and with() give JavaScript’s array methods a consistent story: transformations produce new arrays, and the original is never touched unless you explicitly call a mutating method. That eliminates a class of bugs where a sort or splice on a shared array silently affects other code holding a reference to it — and it removes the need for the spread-then-mutate workaround that’s been standard practice for years.
Keep reading
Takina · · 4 min read Web Streams API Explained: Readable, Writable, Transform
The Streams API lets JavaScript process data as chunks arrive instead of buffering it all in memory. How ReadableStream, WritableStream, and pipes work.
Takina · · 4 min read npm and pnpm Workspaces: Managing a Monorepo
Workspaces let npm and pnpm manage multiple packages in one repository, sharing a single dependency tree and letting packages reference each other locally.
Takina · · 3 min read JavaScript Array.sort() Pitfalls (and How to Fix Them)
Array.sort() converts elements to strings by default, so numbers sort out of order. Here's why, and how a compare function fixes it.