
Immutable Array Transformations with toSorted toReversed and toSpliced
Array operations in JavaScript have long suffered from confusing mutability inconsistencies. While standard utility functions such as map(), filter(), and concat() return brand-new array references, core manipulation methods like sort(), reverse(), and splice() mutate the underlying array in place. In modern reactive frameworks and unidirectional state architectures—including React, Vue, Svelte, and Redux—in-place mutations are a frequent source of bugs. Because object identity remains unaltered when an array is sorted or reversed in place, reference-based change detection mechanisms fail to detect state updates, leading to stale user interfaces and hard-to-trace synchronization discrepancies.
To avoid silent data corruption, developers traditionally relied on defensive shallow copying, prefixing operations with spread syntax or slice calls such as [...items].sort() or items.slice().reverse(). While workable, this approach relies entirely on developer vigilance, clutters business logic, and invites catastrophic bugs whenever someone accidentally omits the defensive spread. The ECMAScript Change Array by Copy specification directly solves this problem by introducing non-destructive array methods: toSorted(), toReversed(), toSpliced(), and with(). These built-in methods mirror the capabilities of their legacy counterparts while returning newly allocated array copies, preserving original data structures without boilerplate.
Example
const scores = [88, 42, 95, 73];
// Non-destructive sorting
const ascending = scores.toSorted((a, b) => a - b);
console.log(ascending); // [42, 73, 88, 95]
console.log(scores); // [88, 42, 95, 73] (Original remains intact)
// Non-destructive reversal
const reversed = scores.toReversed();
console.log(reversed); // [73, 95, 42, 88]
// Non-destructive splice: delete 1 item at index 1 and insert 99
const spliced = scores.toSpliced(1, 1, 99);
console.log(spliced); // [88, 99, 95, 73]
// Single element replacement by index
const updated = scores.with(0, 100);
console.log(updated); // [100, 42, 95, 73]
Implementation
/**
* Real-world catalog store demonstrating immutable array operations
* for filtering, multi-column sorting, pagination, and status toggles.
*/
class ProductCatalogStore {
constructor(initialProducts = []) {
// Internal state remains strictly private and immutable
this.#products = Object.freeze([...initialProducts]);
}
#products;
get products() {
return this.#products;
}
/**
* Returns a sorted product view without mutating catalog state.
*/
sortBy(field, direction = 'asc') {
const modifier = direction === 'desc' ? -1 : 1;
// toSorted returns a new array, leaving this.#products untouched
return this.#products.toSorted((a, b) => {
if (typeof a[field] === 'string') {
return a[field].localeCompare(b[field]) * modifier;
}
return (a[field] - b[field]) * modifier;
});
}
/**
* Promotes featured items to the top by reversing curated product lists.
*/
getInvertedDisplayOrder() {
// toReversed creates a shallow reversed clone
return this.#products.toReversed();
}
/**
* Removes or replaces a product record at a given index without modifying state.
*/
removeProductAt(index) {
if (index < 0 || index >= this.#products.length) {
throw new RangeError(`Index ${index} is out of catalog bounds.`);
}
// toSpliced removes 1 item at specified index and returns the new array
const updatedList = this.#products.toSpliced(index, 1);
return new ProductCatalogStore(updatedList);
}
/**
* Updates a single item's stock or price using the non-destructive with() method.
*/
updateProductField(targetId, field, newValue) {
const targetIndex = this.#products.findIndex(p => p.id === targetId);
if (targetIndex === -1) {
throw new Error(`Product with ID ${targetId} not found.`);
}
const currentItem = this.#products[targetIndex];
const updatedItem = { ...currentItem, [field]: newValue };
// with() replaces element at targetIndex non-destructively
const updatedList = this.#products.with(targetIndex, updatedItem);
return new ProductCatalogStore(updatedList);
}
}
// Verification with sample product catalog
const inventory = [
{ id: 'p1', title: 'Mechanical Keyboard', price: 120, rating: 4.8 },
{ id: 'p2', title: 'Ergonomic Mouse', price: 65, rating: 4.5 },
{ id: 'p3', title: 'UltraWide Monitor', price: 450, rating: 4.9 },
{ id: 'p4', title: 'USB-C Dock', price: 90, rating: 4.2 }
];
const store = new ProductCatalogStore(inventory);
// 1. Test immutable sort
const sortedByPrice = store.sortBy('price', 'asc');
console.log('Cheapest first:', sortedByPrice.map(p => `${p.title} ($${p.price})`));
console.log('Original first item remains:', store.products[0].title); // Mechanical Keyboard
// 2. Test immutable single item update
const updatedStore = store.updateProductField('p2', 'price', 55);
console.log('Updated item price:', updatedStore.products[1].price); // 55
console.log('Original item price unchanged:', store.products[1].price); // 65
// 3. Test immutable deletion with toSpliced
const trimmedStore = store.removeProductAt(0);
console.log('New catalog length:', trimmedStore.products.length); // 3
console.log('Original catalog length intact:', store.products.length); // 4
Main Point: The toSorted toReversed toSpliced and with methods provide native immutable array transformations in JavaScript, eliminating accidental state mutations and the need for defensive array copying.
Credits
Photo by Annie Spratt on Unsplash