JavaScript arrays have long been a cornerstone of the language, but working with them often meant mutating your original data. With ES2023, four new copying methods arrived that change how we think about array manipulation. These methods--toSorted(), toReversed(), toSpliced(), and with()--provide immutable alternatives to their mutating counterparts, enabling safer, more predictable code. Whether you're building complex state management systems or simply want cleaner data transformations, these methods deserve a place in your toolkit.
For teams practicing modern JavaScript development, these methods represent a significant shift toward immutable data patterns that reduce bugs and improve code maintainability.
Each method provides an immutable alternative to a classic mutating operation
toSorted()
The copying version of sort() that returns a new sorted array without modifying the original
toReversed()
The copying version of reverse() that returns a reversed array while preserving the source
toSpliced()
The copying version of splice() that enables adding, removing, or replacing elements immutably
with()
The copying version of index assignment that updates a single element without mutation
Why Copying Methods Matter
The Evolution of Array Manipulation
Traditional mutating methods like sort(), reverse(), splice(), and direct index assignment have been JavaScript staples for years. However, they share a dangerous characteristic: they modify your original data directly. This mutating behavior leads to unexpected side effects when arrays are shared across different parts of your application.
In modern JavaScript development, immutable operations have become the preferred approach. Frameworks like React, Vue, and Angular encourage or require immutable state updates. Functional programming principles emphasize creating new data structures rather than modifying existing ones. These trends created a clear need for copying alternatives to JavaScript's classic mutating array methods.
The ES2023 specification addressed this need by introducing four copying methods that provide the same functionality as their mutating counterparts while ensuring your original data remains untouched. This seemingly simple change has profound implications for code reliability and predictability.
Benefits of Immutability
- Predictable Behavior: Functions that don't modify their inputs are easier to reason about and debug.
- Shared State Safety: When arrays are passed between functions or components, immutability prevents unexpected changes.
- Change Tracking: Immutable structures make it simple to track what changed and when.
- Undo/Redo Functionality: Immutable operations naturally support undo and redo capabilities.
- Functional Programming Alignment: Immutability enables powerful patterns like map, filter, and reduce.
For applications requiring robust data integrity, adopting these immutable patterns pays dividends in code quality and system reliability.
toSorted(): Sorting Without Mutation
The toSorted() method is the copying version of sort(). Unlike its predecessor, which rearranges elements directly within the original array, toSorted() returns a brand new array with elements arranged in ascending order while leaving your source data completely untouched.
const numbers = [5, 2, 8, 1, 9];
const sorted = numbers.toSorted((a, b) => a - b);
console.log(sorted); // [1, 2, 5, 8, 9]
console.log(numbers); // [5, 2, 8, 1, 9] - unchanged
How It Works
When you call toSorted(), the method creates a fresh array instance, applies the sorting algorithm, and returns it. The original array remains completely unchanged throughout this process. This behavior aligns perfectly with immutable programming principles.
The method accepts an optional compare function parameter, just like sort(), allowing you to define custom sorting logic:
- Numeric sorting:
(a, b) => a - b - Descending order:
(a, b) => b - a - String sorting:
(a, b) => a.localeCompare(b) - Object property sorting:
(a, b) => a.name.localeCompare(b.name)
Beyond Basic Sorting
The toSorted() method shines in real-world scenarios where data integrity matters. Consider a dashboard displaying customer orders that must remain chronologically ordered for audit purposes while allowing users to sort by price for analysis:
const orders = [
{ id: 1, customer: 'Alice', total: 150, date: '2024-01-15' },
{ id: 2, customer: 'Bob', total: 85, date: '2024-01-10' },
{ id: 3, customer: 'Carol', total: 200, date: '2024-01-12' }
];
// Display in original order for audit trail
auditDisplay.textContent = JSON.stringify(orders, null, 2);
// Allow user to sort by total without affecting audit data
function sortByTotal() {
const sortedOrders = orders.toSorted((a, b) => a.total - b.total);
return sortedOrders;
}
In this scenario, the original orders array remains pristine for any audit or compliance requirements, while users get their sorted view without any risk of corrupting the source data. This pattern proves invaluable when building web applications that handle sensitive or regulated data. Implementing proper data handling practices ensures your applications remain compliant and reliable.
toReversed(): Preserving Your Original Array
The toReversed() method addresses a common need--viewing array elements in opposite order--without the destructive side effects of reverse(). Before this method existed, reversing an array meant permanently altering its contents, which often led to unexpected bugs in larger applications.
const tasks = ['Design', 'Develop', 'Test', 'Deploy'];
const reversed = tasks.toReversed();
console.log(reversed); // ['Deploy', 'Test', 'Develop', 'Design']
console.log(tasks); // ['Design', 'Develop', 'Test', 'Deploy'] - unchanged
Common Use Cases
- Recent Activity Feeds: Display items in reverse chronological order
- Undo/Redo Stacks: Manage navigation history without losing original order
- Mathematical Computations: Traverse arrays in reverse for specific algorithms
- Mirror-Image Views: Show data from different perspectives simultaneously
Real-World Implementation
In a collaborative editing application, you might need to show both the original document order and a reverse chronological view of comments simultaneously:
const comments = [
{ id: 1, author: 'Alice', text: 'First comment', timestamp: 1000 },
{ id: 2, author: 'Bob', text: 'Reply to Alice', timestamp: 2000 },
{ id: 3, author: 'Carol', text: 'Reply to Bob', timestamp: 3000 }
];
// Display in chronological order (as written)
chronologicalList.render(comments);
// Display in reverse order (newest first)
reverseList.render(comments.toReversed());
// Both views work from the same source without conflicts
This pattern becomes essential when building modern web applications that require multiple synchronized views of the same data. The ability to create reversed copies without mutation eliminates an entire class of bugs related to shared state corruption. Adopting these immutable data patterns is a hallmark of mature development practices.
toSpliced(): Safe Array Modifications
The splice() method has long been JavaScript's workhorse for adding, removing, or replacing array elements. However, its mutating nature makes it a frequent source of bugs. toSpliced() brings the same powerful functionality in a copying form.
const colors = ['red', 'green', 'blue', 'yellow'];
const modified = colors.toSpliced(1, 2, 'purple', 'orange');
console.log(modified); // ['red', 'purple', 'orange', 'yellow']
console.log(colors); // ['red', 'green', 'blue', 'yellow'] - unchanged
Understanding the Parameters
toSpliced() accepts the same parameters as splice():
- start: The index at which to begin changing the array
- deleteCount: The number of elements to remove
- ...items: Optional elements to insert at the start position
Practical Applications
The toSpliced() method handles three distinct scenarios with a single, elegant API:
Deletion: Removing items without mutating state
const items = ['apple', 'banana', 'cherry', 'date'];
const withoutBanana = items.toSpliced(1, 1);
console.log(withoutBanana); // ['apple', 'cherry', 'date']
console.log(items); // ['apple', 'banana', 'cherry', 'date']
Insertion: Adding elements at specific positions
const weekdays = ['Mon', 'Tue', 'Thu', 'Fri'];
const withWednesday = weekdays.toSpliced(2, 0, 'Wed');
console.log(withWednesday); // ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
Replacement: Swapping elements cleanly
const oldTech = ['React', 'Angular', 'Ember'];
const updated = oldTech.toSpliced(1, 1, 'Vue');
console.log(updated); // ['React', 'Vue', 'Ember']
These patterns prove invaluable when building e-commerce platforms where cart modifications, wishlist updates, and inventory changes all require immutable transformations. Implementing proper state management through modern JavaScript practices ensures your applications remain scalable and maintainable.
with(): Precise Index-Based Updates
The with() method provides the most granular level of copying array modification. While other methods operate on ranges or entire arrays, with() targets a specific index, replacing its value while creating a new array containing all other elements unchanged.
const scores = [95, 87, 92, 88];
const updated = scores.with(1, 90);
console.log(updated); // [95, 90, 92, 88]
console.log(scores); // [95, 87, 92, 88] - unchanged
The Before and After
Before with(), updating a single array element immutably required verbose workarounds:
// Old approach - verbose and error-prone
const original = [1, 2, 3, 4];
const updated = [...original.slice(0, 2), 99, ...original.slice(3)];
// New approach - clean and readable
const original = [1, 2, 3, 4];
const updated = original.with(2, 99);
Ideal Use Cases
- Form Field Updates: Modify specific inputs without mutating entire form state
- React State Updates: Update array elements in functional component state
- Configuration Changes: Modify specific settings in configuration arrays
- Game State: Update individual game piece positions immutably
Working with Negative Indices
Like other array methods, with() supports negative indices for end-relative positioning:
const data = [10, 20, 30, 40, 50];
// Update the last element
const updated = data.with(-1, 99);
// [10, 20, 30, 40, 99]
// Update the second-to-last element
const updated2 = data.with(-2, 'modified');
// [10, 20, 30, 'modified', 50]
Real-World Form Handling
In a form with dynamic fields, with() provides clean syntax for updating individual field values:
const formFields = [
{ name: 'email', value: '[email protected]', valid: true },
{ name: 'phone', value: '', valid: false },
{ name: 'name', value: 'John', valid: true }
];
function updateField(index, newValue) {
return formFields.with(index, {
...formFields[index],
value: newValue,
valid: validateField('name', newValue)
});
}
This pattern ensures that when building custom web applications, form state updates remain predictable and debuggable, with each change producing a new state rather than silently mutating existing state. Teams investing in React development services find these patterns especially valuable for state management.
Combining the New Methods
The true power of these new methods emerges when you chain them together. Because each method returns a new array, you can build complex transformation pipelines that remain immutable throughout.
Method Chaining Examples
const data = [3, 1, 4, 1, 5, 9, 2, 6];
// Sort, reverse, and update a specific element
const result = data
.toSorted((a, b) => a - b)
.toReversed()
.with(0, 7);
console.log(result); // [7, 6, 5, 4, 3, 2, 1, 1]
console.log(data); // [3, 1, 4, 1, 5, 9, 2, 6] - completely unchanged
Real-World Transformation Pipeline
Consider a product management system where you need to apply multiple transformations:
const products = [
{ name: 'Widget A', price: 100, category: 'electronics', active: true },
{ name: 'Widget B', price: 50, category: 'home', active: false },
{ name: 'Widget C', price: 75, category: 'electronics', active: true },
{ name: 'Widget D', price: 200, category: 'electronics', active: true }
];
// Filter active electronics, sort by price descending, highlight the most expensive
const featured = products
.filter(p => p.active && p.category === 'electronics')
.toSorted((a, b) => b.price - a.price)
.with(0, { ...products[0], featured: true });
Migration Patterns
When migrating from mutating to copying methods in existing codebases, a systematic approach minimizes risk:
Phase 1: Inventory: Identify all mutating array method calls in your codebase. Pay special attention to arrays that are parameters, class properties, or global state.
Phase 2: Prioritize: Start with arrays that are shared between functions or components. These carry the highest risk of mutation-related bugs.
Phase 3: Replace Incrementally: Replace one method at a time, adding comments that explain the immutability benefit:
// Before: mutating approach
items.sort((a, b) => a.name.localeCompare(b.name));
// After: copying approach (safer for shared data)
const sortedItems = items.toSorted((a, b) => a.name.localeCompare(b.name));
// Original items preserved for other consumers
Phase 4: Test Thoroughly: Add tests that verify original arrays remain unchanged after transformations. This catches regressions early.
Phase 5: Performance Validation: For performance-critical code paths, benchmark the changes. Modern JavaScript engines handle array copying efficiently, but extreme cases warrant verification.
For teams building scalable web applications, this migration investment pays dividends in code reliability and team velocity. Implementing these modern development practices positions your codebase for future growth and easier maintenance.
Browser Support and Compatibility
Current Availability
These four copying methods were introduced in ES2023 and have achieved broad browser support:
- Chrome/Edge: Version 110+ (February 2023)
- Firefox: Version 115+ (July 2023)
- Safari: Version 16.4+ (March 2023)
All major modern browsers now support these methods, making them safe to use in production applications targeting contemporary browsers.
Polyfill Strategies
For environments that don't support these methods natively, polyfills are available through the core-js library:
// Using core-js
import 'core-js/actual/array/to-sorted';
import 'core-js/actual/array/to-reversed';
import 'core-js/actual/array/to-spliced';
import 'core-js/actual/array/with';
// Feature detection pattern
if (typeof Array.prototype.toSorted !== 'function') {
// Apply polyfills or use alternative implementations
require('core-js/actual/array/to-sorted');
}
Build tools like Babel can automatically include polyfills based on your target browser list. Configure your browserslist in package.json to specify supported environments:
{
"browserslist": [
"last 2 Chrome versions",
"last 2 Firefox versions",
"last 2 Safari versions",
"last 2 Edge versions"
]
}
Performance Considerations
While copying methods create new arrays, modern JavaScript engines optimize these operations effectively. The performance characteristics vary by engine:
- V8 (Chrome/Edge): Highly optimized, minimal overhead for typical array sizes
- SpiderMonkey (Firefox): Good performance for common use cases
- JavaScriptCore (Safari): Efficient implementation with reasonable memory characteristics
For most applications, the benefits of immutability outweigh the minor overhead of array allocation. The clarity and safety gains typically justify the small memory cost.
When to consider performance implications: In tight loops processing large arrays (10,000+ elements), or in performance-critical code paths executing thousands of times per second, you may want to benchmark and potentially stick with mutating operations or implement targeted optimizations.
If you're unsure whether your application falls into these edge cases, profile your specific use case rather than preemptively avoiding these valuable methods. The MDN documentation provides additional performance context for each method. For teams prioritizing performance optimization, understanding these trade-offs is essential for making informed architectural decisions.
Best Practices and Recommendations
When to Choose Copying Methods
Prefer copying methods when:
- Arrays are shared between components or functions
- You need to maintain historical snapshots for undo/redo
- Working with React, Vue, or other stateful frameworks
- Building undo/redo functionality or need change tracking
- Following functional programming principles
- Debugging would benefit from data stability and predictability
- Data integrity and compliance requirements exist
Mutating methods may be acceptable when:
- Working with local variables that aren't shared anywhere
- Performance is critical and arrays are large (benchmark first)
- Immediate in-place modification is required for memory constraints
- Working with legacy code that expects mutating behavior and migration is impractical
Decision Framework
function processData(data, isShared) {
if (isShared) {
// Use copying methods for shared/external data
const sorted = data.toSorted();
return sorted;
} else {
// Local temporary processing can use mutating methods
const temp = [...data];
temp.sort(() => Math.random() - 0.5);
return temp;
}
}
Common Pitfalls to Avoid
- Forgetting Return Values: Copying methods return new arrays. If you don't capture the return value, nothing happens:
// Wrong: return value ignored
[1, 3, 2].toSorted();
// Correct: capture the new array
const sorted = [1, 3, 2].toSorted();
-
Over-Mutation in Hot Paths: Not every array needs immutability. Use copying methods where it matters most.
-
Nested Mutation: Even with copying methods, nested objects can still mutate:
const users = [{ name: 'Alice', settings: { theme: 'dark' } }];
const updated = users.with(0, { ...users[0], settings: { theme: 'light' } });
// Need to spread nested objects too for full immutability
Code Review Checklist
- Are array modifications creating new arrays when shared data is involved?
- Is the original data preserved after transformations?
- Are chained transformations readable and maintainable?
- Have performance implications been considered for critical paths?
- Are polyfills included for required browser support?
- Are nested objects also handled immutably when needed?
By following these guidelines, teams can confidently adopt ES2023's copying array methods while maintaining code quality and performance. These methods represent a significant step forward for JavaScript, enabling cleaner, more predictable code across enterprise web applications. Partnering with experienced JavaScript development teams ensures proper implementation and code review processes are in place.