Immutable Array Updates Made Easy: Meet Array.prototype.with()

Seasoned web developer since 2014, specializing in React, Node, and Typescript. Expertise spans from MeteorJs to NextJs and Ionic. Formerly with ZadGroup, I crafted web and mobile solutions for 6 years. Pivoted to backend development in 2020 with RoaaTech, expanding into development, DevOps, and team leadership. Active freelancer with a strong reputation on Upwork and a dedicated client base in Turkey. Committed to continuous learning and leveraging the best tools and practices in the industry.
As JavaScript developers, especially when working with state management in frameworks like React, Vue, or Angular, or simply aiming for more predictable code, immutability is a key concept. Modifying an array "in place" can lead to unexpected side effects. The traditional way to update an element in an array immutably often involved methods like slice(), map(), or the spread operator, which could sometimes feel a bit clunky for a simple targeted update.
Enter Array.prototype.with(index, value), a new method introduced in ECMAScript 2023 (ES14). It provides a clean, direct, and immutable way to get a new array with an element at a specific index replaced with a new value. [2, 6]
The Old Way vs. The New Way
Let's say you have an array of tasks, and you want to update the status of a specific task without mutating the original array.
Before Array.prototype.with():
const tasks = [
{ id: 1, text: "Learn JavaScript", status: "done" },
{ id: 2, text: "Write an article", status: "in-progress" },
{ id: 3, text: "Deploy to production", status: "todo" },
];
const taskIndexToUpdate = 1;
const newStatus = "done";
// Common approach using map:
const updatedTasksMap = tasks.map((task, index) => {
if (index === taskIndexToUpdate) {
return { ...task, status: newStatus };
}
return task;
});
console.log("Original tasks (map):", tasks);
console.log("Updated tasks (map):", updatedTasksMap);
/*
Original tasks (map): [
{ id: 1, text: 'Learn JavaScript', status: 'done' },
{ id: 2, text: 'Write an article', status: 'in-progress' },
{ id: 3, text: 'Deploy to production', status: 'todo' }
]
Updated tasks (map): [
{ id: 1, text: 'Learn JavaScript', status: 'done' },
{ id: 2, text: 'Write an article', status: 'done' }, // Updated
{ id: 3, text: 'Deploy to production', status: 'todo' }
]
*/
// Another approach using slice and spread (more verbose for single element):
const updatedTasksSlice = [
...tasks.slice(0, taskIndexToUpdate),
{ ...tasks[taskIndexToUpdate], status: newStatus },
...tasks.slice(taskIndexToUpdate + 1),
];
console.log("Updated tasks (slice):", updatedTasksSlice);
While these methods work and preserve immutability, they require a bit more boilerplate, especially the slice method for a single update. The map approach iterates over the entire array, which is fine for many cases but less direct if you know the index.
With Array.prototype.with():
Now, let's see how with() simplifies this:
const tasks = [
{ id: 1, text: "Learn JavaScript", status: "done" },
{ id: 2, text: "Write an article", status: "in-progress" },
{ id: 3, text: "Deploy to production", status: "todo" },
];
const taskIndexToUpdate = 1;
const newStatus = "done";
// Using .with()
const updatedTasksWith = tasks.with(taskIndexToUpdate, {
...tasks[taskIndexToUpdate], // Spread the existing task to only update status
status: newStatus,
});
console.log("Original tasks (with):", tasks); // Unchanged!
console.log("Updated tasks (with):", updatedTasksWith);
/*
Original tasks (with): [
{ id: 1, text: 'Learn JavaScript', status: 'done' },
{ id: 2, text: 'Write an article', status: 'in-progress' },
{ id: 3, text: 'Deploy to production', status: 'todo' }
]
Updated tasks (with): [
{ id: 1, text: 'Learn JavaScript', status: 'done' },
{ id: 2, text: 'Write an article', status: 'done' }, // Updated
{ id: 3, text: 'Deploy to production', status: 'todo' }
]
*/
Notice how tasks.with(index, newValue) clearly expresses the intent: "give me a new array based on tasks, but with the element at index changed to newValue." The original tasks array remains untouched. [2, 6]
Why is Array.prototype.with() a "Very Useful Tweak"?
Guaranteed Immutability: Its primary purpose is to return a new array, reinforcing immutable patterns. This helps prevent bugs related to shared mutable state. [2]
Readability & Conciseness: The code
arr.with(index, value)is very declarative and easy to understand at a glance compared to manual slicing or mapping for a single element replacement.Reduced Cognitive Load: You don't have to mentally parse more complex constructions like
slice/concator amapfunction just to change one item.Method Chaining: Like other modern array methods that return new arrays (e.g.,
map,filter),with()can be easily chained.Consistency: It joins other new immutable array methods like
toSorted(),toReversed(), andtoSpliced(), providing a more consistent API for immutable operations. [6]
Important Considerations
Shallow Copy for the New Value: If the
newValueyou provide towith()is an object or array,with()itself doesn't deep clone it. If you're updating an object within the array (like in ourtasksexample), you still need to ensure you're providing a new object for that element (e.g., using spread syntax{ ...oldObject, propertyToChange: newValue }) if you want to avoid mutating the original nested object. Thewith()method handles the immutability of the array itself.Index Out of Bounds: If the
indexis out of bounds,with()will throw aRangeError, similar to how trying to assign to an out-of-bounds index with strict mode might behave, but more explicit. [2]
Browser Support and Polyfills
As an ES2023 feature, Array.prototype.with() is available in the latest versions of modern browsers and Node.js. [2, 6] For older environments, you'll need to use a transpiler like Babel along with a polyfill (e.g., from core-js) to use it. [5]
Conclusion
Array.prototype.with() is a welcome addition for JavaScript developers who value immutability and clean code. It simplifies a common array manipulation pattern, making your code more readable, less error-prone, and more aligned with functional programming principles. It's a small change that can make a big difference in the clarity and robustness of your array operations!



