Blog post

ES2023 introduces new array copying methods to JavaScript

Blog Author Phil Nash

Phil Nash

Developer Advocate JS/TS

Date

  • JavaScript

The ECMAScript 2023 specification has been recently finalised. It includes some new methods on the Array object that will help make our JavaScript programs more predictable and maintainable. The methods toSorted, toReversed, toSpliced, and with allow you to perform operations on arrays by without changing the data in place, but by making a copy and changing that copy. Read on to learn the difference and how to start using them in your projects.

Mutation and side effects

The Array object has always had some oddities. Methods like sort, reverse, and splice change the array in place. Other methods like concat, map, and filter create a copy of the array and then operate on the copy. When you perform an operation on an object that mutates it, that is a side effect and can cause unexpected behaviour elsewhere in your system.

As an example, this is what happens when you reverse an array.

const languages = ["JavaScript", "TypeScript", "CoffeeScript"];
const reversed = languages.reverse();
console.log(reversed);
// => [ 'CoffeeScript', 'TypeScript', 'JavaScript' ]
console.log(languages);
// => [ 'CoffeeScript', 'TypeScript', 'JavaScript' ]
console.log(Object.is(languages, reversed));
// => true

As you can see, the original array was reversed and even though we assigned the result of reversing the array to a new variable, both variables simply point to the same array.

Mutating arrays and React

One of the best known issues with array methods that mutate the array is when you use them in a React component. You can't mutate an array and then try to set it as a new state because the array itself is the same object and this won't trigger a new render. Instead you need to copy the array first, then mutate the copy and set that as the new state. The React docs have a whole page explaining how to update arrays in state because of this.

Copy first, then mutate

The way to work around this has been to copy the array first, then mutate it. There are several different ways to make a copy of an array, including: Array.from, the spread operator, or calling the slice function with no arguments.

const languages = ["JavaScript", "TypeScript", "CoffeeScript"];
const reversed = Array.from(languages).reverse();
// => [ 'CoffeeScript', 'TypeScript', 'JavaScript' ]
console.log(languages);
// => [ 'JavaScript', 'TypeScript', 'CoffeeScript' ]
console.log(Object.is(languages, reversed));
// => false

It's great that there is a workaround, but having to remember to perform one of the different copy methods first isn't great.

New methods change by copy

That's where the new methods come in. Each of toSorted, toReversed, toSpliced, and with copy the original array for you, change the copy and return it. It will make performing each of these actions easier to write as you only need to remember to call one function and easier to read as you don't need to parse one of four methods of copying an array first. So what do each of the methods do?

Array.prototype.toSorted

The toSorted function returns a new, sorted array.

const languages = ["JavaScript", "TypeScript", "CoffeeScript"];
const sorted = languages.toSorted();
console.log(sorted);
// => [ 'CoffeeScript', 'JavaScript', 'TypeScript' ]
console.log(languages);
// => [ 'JavaScript', 'TypeScript', 'CoffeeScript' ]

The sort function has some unexpected behaviour and aside from copying, toSorted shares that behaviour. You still need to be careful if you are sorting numbers or strings with accented characters. Make sure you provide a comparator function (like String's localeCompare) that will produce the results you are looking for.

const numbers = [5, 3, 10, 7, 1];
const sorted = numbers.toSorted();
console.log(sorted);
// => [ 1, 10, 3, 5, 7 ]
const sortedCorrectly = numbers.toSorted((a, b) => a - b);
console.log(sortedCorrectly);
// => [ 1, 3, 5, 7, 10 ]


const strings = ["abc", "äbc", "def"];
const sorted = strings.toSorted();
console.log(sorted);
// => [ 'abc', 'def', 'äbc' ]
const sortedCorrectly = strings.toSorted((a, b) => a.localeCompare(b));
console.log(sortedCorrectly);
// => [ 'abc', 'äbc', 'def' ]

Array.prototype.toReversed

Using the toReversed function returns a new array sorted in the reverse order.

const languages = ["JavaScript", "TypeScript", "CoffeeScript"];
const reversed = languages.toReversed();
console.log(reversed);
// => [ 'CoffeeScript', 'TypeScript', 'JavaScript' ]


Sonar has a rule that covers misleading use of methods like reverse. Assigning the result of reverse to a new variable is misleading because the original array was mutated too. Now you can use toReversed or toSorted to copy the array and mutate the copy instead..

Array.prototype.toSpliced

The toSpliced function is a bit different to its original version splice. splice changes the existing array by deleting and adding elements at the provided index and returns an array containing the deleted elements from the array. toSpliced returns a new array without the removed elements and including any added elements. Here's how it works:

const languages = ["JavaScript", "TypeScript", "CoffeeScript"];
const spliced = languages.toSpliced(2, 1, "Dart", "WebAssembly");
console.log(spliced);
// => [ 'JavaScript', 'TypeScript', 'Dart', 'WebAssembly' ]

If you are using splice for its return value, then toSpliced will not be a drop in replacement. If you want to know the deleted elements without altering the original array, then you should use the copying method slice.

Frustratingly, splice takes different arguments to slice. splice takes an index and the number of elements after that index to remove and slice takes two indexes, the start and the end. If you wanted to use toSpliced in place of splice but also get the elements that are deleted you could apply toSpliced and slice to the original array, like this:

const languages = ["JavaScript", "TypeScript", "CoffeeScript"];
const startDeletingAt = 2;
const deleteCount = 1;
const spliced = languages.toSpliced(startDeletingAt, deleteCount, "Dart", "WebAssembly");
const removed = languages.slice(startDeletingAt, startDeletingAt + deleteCount);
console.log(spliced);
// => [ 'JavaScript', 'TypeScript', 'Dart', 'WebAssembly' ]
console.log(removed);
// => [ 'CoffeeScript' ]

Array.prototype.with

The with function is the copy equivalent of using square bracket notation to change one element of an array. So, instead of directly changing the array like this:

const languages = ["JavaScript", "TypeScript", "CoffeeScript"];
languages[2] = "WebAssembly";
console.log(languages);
// => [ 'JavaScript', 'TypeScript', 'WebAssembly' ]

You can copy the array and make the change

const languages = ["JavaScript", "TypeScript", "CoffeeScript"];
const updated = languages.with(2, "WebAssembly");
console.log(updated);
// => [ 'JavaScript', 'TypeScript', 'WebAssembly' ]
console.log(languages);
// => [ 'JavaScript', 'TypeScript', CoffeeScript' ]

Not just arrays

The regular array object isn't the only one benefitting from these new methods. You can also use toSorted, toReversed, and with on any TypedArray. That is everything from Int8Array to BigUint64Array. TypedArrays do not have a splice method, so they are not getting a matching toSpliced method.

Caveats

I mentioned at the top that methods like map, filter, and concat already perform copying operations. There is a difference between those methods and the new copying methods though. If you extend the built in Array object and use map, flatMap, filter, or concat on an instance, it will return a new instance of the same type. If you extend an Array and use toSorted, toReversed, toSpliced, or with the result will be a plain Array again.

class MyArray extends Array {}
const languages = new MyArray("JavaScript", "TypeScript", "CoffeeScript");
const upcase = languages.map(language => language.toUpperCase());
console.log(upcase instanceof MyArray);
// => true
const reversed = languages.toReversed();
console.log(reversed instanceof MyArray);
// => false

You can turn that back into your custom Array with the use of MyArray.from:

class MyArray extends Array {}
const languages = new MyArray("JavaScript", "TypeScript", "CoffeeScript");
const reversed = MyArray.from(languages.toReversed());
console.log(reversed instance of MyArray);
// => true

Support

While the ECMAScript 2023 spec is very new, there is already good support for these new array methods. Chrome 110, Safari 16.3, Node.js 20, and Deno 1.31 all support all four methods and there are polyfills and shims available for platforms that don't yet have support.

JavaScript keeps improving

It's great to see additions like this to the ECMAScript standard that make it easier for us to write predictable code. There are a few other proposals that made it into ES2023 that you should check out if you're interested. Check out the whole TC39 proposals repository if you want to see what else is close to joining the spec.

Get new blogs delivered directly to your inbox!

Stay up-to-date with the latest Sonar content. Subscribe now to receive the latest blog articles. 

the Sonar solution

SonarLint

Clean Code from the start in your IDE

Up your coding game and discover issues early. SonarLint takes linting to another level empowering you to find & fix issues in real time.

Install SonarLint -->
SonarQube

Clean Code for teams and enterprises

Empower development teams with a self-hosted code quality and security solution that deeply integrates into your enterprise environment; enabling you to deploy clean code consistently and reliably.

Download SonarQube -->
SonarCloud

Clean Code in your cloud workflow

Enable your team to deliver clean code consistently and efficiently with a code review tool that easily integrates into the cloud DevOps platforms and extend your CI/CD workflow.

Try SonarCloud -->