Immutable Array Updates with Array.prototype.with

The Array.prototype.with() method, introduced in ECMAScript 2023 (ES14), allows developers to update a specific element in an array by index without modifying the original array. Instead of mutating the source in place, it creates and returns a shallow copy of the array with the specified index updated to the new value. This article explains how the method works, its syntax, key characteristics, and how it simplifies immutable state management in modern JavaScript.

Syntax and Basic Usage

The syntax for Array.prototype.with() is straightforward:

array.with(index, value)

Example: Mutable vs. Immutable Updates

Traditionally, updating an array element directly with bracket notation mutates the original array:

const original = ['apple', 'banana', 'cherry'];

// Mutable approach (modifies 'original')
original[1] = 'blueberry';
console.log(original); // ['apple', 'blueberry', 'cherry']

With Array.prototype.with(), the original array remains untouched, and a new array is returned:

const fruits = ['apple', 'banana', 'cherry'];

const updatedFruits = fruits.with(1, 'blueberry');

console.log(fruits);        // ['apple', 'banana', 'cherry'] (unchanged)
console.log(updatedFruits); // ['apple', 'blueberry', 'cherry']

Key Characteristics of Array.prototype.with()

1. Negative Indexing

Unlike bracket notation (arr[arr.length - 1]), .with() natively supports negative indices:

const numbers = [10, 20, 30, 40];
const modified = numbers.with(-1, 99);

console.log(modified); // [10, 20, 30, 99]

2. Error Handling on Out-of-Bounds Indices

If the specified index is greater than or equal to array.length, or if negative indexing extends beyond the start of the array, Array.prototype.with() throws a RangeError:

const items = ['a', 'b'];

items.with(5, 'c');  // Throws RangeError: Invalid index : 5
items.with(-3, 'c'); // Throws RangeError: Invalid index : -3

3. Handling Sparse Arrays

When used on a sparse array (an array containing empty slots), .with() creates a dense array where any empty slots become undefined:

const sparse = [1, , 3]; // Contains an empty slot at index 1
const dense = sparse.with(0, 10);

console.log(dense); // [10, undefined, 3]

Why Use Array.prototype.with()?

Before this method, achieving immutability required spreading the array or using slicing techniques:

// Pre-ES2023 workaround
const updated = [
  ...original.slice(0, targetIndex),
  newValue,
  ...original.slice(targetIndex + 1)
];

Array.prototype.with() eliminates boilerplate code, reduces cognitive overhead, and improves readability. It is particularly useful in environments relying on immutable state patterns, such as React, Redux, or functional programming pipelines.