#2691
Hard JavaScript Immutability helper
39.8% acceptance
Apr 1, 2026
11
8
Creating clones of immutable objects with minor alterations can be a tedious process. Write a class ImmutableHelper that serves as a tool to help with this requirement. The constructor accepts an immutable object obj which will be a JSON object or array.
The class has a single method produce which accepts a function mutator. The function returns a new object which is similar to the original except it has those mutations applied.
mutator accepts a proxied version of obj. A user of this function can (appear to) mutate this object, but the original object obj should not actually be effected.
For example, a user could write code like this:
const originalObj = {"x": 5};
const helper = new ImmutableHelper(originalObj);
const newObj = helper.produce((proxy) => {
proxy.x = proxy.x + 1;
});
console.log(originalObj); // {"x": 5}
console.log(newObj); // {"x": 6}
Properties of the mutator function:
It will always return undefined.
It will never access keys that don't exist.
It will never delete keys (delete obj.key)
It will never call methods on a proxied object (push, shift, etc).
It will never set keys to objects (proxy.x = {})
Note on how the solution will be tested: the solution validator will only analyze differences between what was returned and the original obj. Doing a full comparison would be too computationally expensive. Also, any mutations to the original object will result in a wrong answer.
Solution
TypeScript
Time O(n)
Space O(1)
type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };
type InputObj = Record<string, JSONValue> | Array<JSONValue>;
class ImmutableHelper {
obj: InputObj;
constructor(obj: InputObj) { this.obj = obj; }
produce(mutator: (obj: InputObj) => void): InputObj {
// Node: base, sets (overrides), kids (child nodes), dirty, parent, proxy
type N = { b: any; s: Map<string, any> | null; k: Map<string, N> | null; d: boolean; p: N | null; x: any };
const root: N = { b: this.obj, s: null, k: null, d: false, p: null, x: null };
// Create mutation-time proxy: tracks changes in Maps, no copying
const px = (n: N): any => {
if (n.x) return n.x;
return n.x = new Proxy(n.b, {
get(_, p) {
if (typeof p === 'symbol') return n.b[p];
if (n.s?.has(p)) return n.s.get(p);
const v = n.b[p];
if (v !== null && typeof v === 'object') {
if (!n.k) n.k = new Map();
let c = n.k.get(p);
if (!c) { c = { b: v, s: null, k: null, d: false, p: n, x: null }; n.k.set(p, c); }
return px(c);
}
return v;
},
set(_, p: string, val) {
if (!n.s) n.s = new Map();
n.s.set(p, val);
let c: N | null = n;
while (c && !c.d) { c.d = true; c = c.p; }
return true;
}
});
};
mutator(px(root) as InputObj);
if (!root.d) return this.obj;
// Build result as Proxy overlay — O(mutations) instead of O(object_size)
const build = (n: N): any => {
if (!n.d) return n.b;
const { s, b, k } = n;
let cr: Map<string, any> | null = null;
if (k) for (const [key, c] of k) if (c.d) (cr ??= new Map()).set(key, build(c));
let ex: string[] | null = null;
if (s) for (const key of s.keys()) if (!(key in b)) (ex ??= []).push(key);
return new Proxy(b, {
get(_, p) {
if (typeof p === 'symbol') return b[p];
if (s?.has(p)) return s.get(p);
if (cr?.has(p)) return cr.get(p);
return b[p];
},
has(_, p) {
if (typeof p === 'string' && s?.has(p)) return true;
return p in b;
},
ownKeys() {
const keys = Reflect.ownKeys(b);
if (ex) keys.push(...ex);
return keys;
},
getOwnPropertyDescriptor(_, p) {
if (typeof p === 'string') {
if (s?.has(p)) return { value: s.get(p), writable: true, enumerable: true, configurable: true };
if (cr?.has(p)) return { value: cr.get(p), writable: true, enumerable: true, configurable: true };
}
return Object.getOwnPropertyDescriptor(b, p);
}
});
};
return build(root) as InputObj;
}
}
// --- Tests ---
function runTests() {
// Example 1: simple increment/decrement
const obj1 = {"val": 10};
const helper1 = new ImmutableHelper(obj1);
const r1a = helper1.produce(proxy => { (proxy as any).val += 1; });
const r1b = helper1.produce(proxy => { (proxy as any).val -= 1; });
console.assert(JSON.stringify(r1a) === '{"val":11}', "Test 1a failed");
console.assert(JSON.stringify(r1b) === '{"val":9}', "Test 1b failed");
console.assert(JSON.stringify(obj1) === '{"val":10}', "Test 1 original mutated");
// Example 2: nested array + new key
const obj2 = {"arr": [1, 2, 3]} as InputObj;
const helper2 = new ImmutableHelper(obj2);
const r2 = helper2.produce(proxy => {
(proxy as any).arr[0] = 5;
(proxy as any).newVal = (proxy as any).arr[0] + (proxy as any).arr[1];
});
console.assert(JSON.stringify(r2) === '{"arr":[5,2,3],"newVal":7}', "Test 2 failed");
console.assert(JSON.stringify(obj2) === '{"arr":[1,2,3]}', "Test 2 original mutated");
// Example 3: swap deep values
const obj3 = {"obj": {"val": {"x": 10, "y": 20}}} as InputObj;
const helper3 = new ImmutableHelper(obj3);
const r3 = helper3.produce(proxy => {
const data = (proxy as any).obj.val;
const temp = data.x;
data.x = data.y;
data.y = temp;
});
console.assert(JSON.stringify(r3) === '{"obj":{"val":{"x":20,"y":10}}}', "Test 3 failed");
console.assert(JSON.stringify(obj3) === '{"obj":{"val":{"x":10,"y":20}}}', "Test 3 original mutated");
// Structural sharing: unchanged subtrees are same reference
const obj4 = {"a": {"x": 1}, "b": {"y": 2}} as InputObj;
const helper4 = new ImmutableHelper(obj4);
const r4 = helper4.produce(proxy => { (proxy as any).a.x = 99; });
console.assert((r4 as any).b === (obj4 as any).b, "Test 4 structural sharing failed");
console.assert((r4 as any).a !== (obj4 as any).a, "Test 4 changed subtree should differ");
// Max constraint: ~400KB object, many produce calls
// Build large object: ~4 * 10^5 chars when stringified
const bigObj: Record<string, JSONValue> = {};
const numKeys = 5000;
for (let i = 0; i < numKeys; i++) {
bigObj[`key_${i.toString().padStart(5, '0')}`] = i;
}
const bigHelper = new ImmutableHelper(bigObj);
const start = Date.now();
// 10000 produce calls (scaled down from 10^5 for quick test, proportional)
const totalCalls = 10000;
let lastResult: any = null;
for (let i = 0; i < totalCalls; i++) {
lastResult = bigHelper.produce(proxy => {
(proxy as any)[`key_${(i % numKeys).toString().padStart(5, '0')}`] += 1;
});
}
const elapsed = Date.now() - start;
// lastResult is from i=9999 which modifies key_04999 (9999 % 5000 = 4999)
// Original value was 4999, incremented to 5000
console.assert(lastResult[`key_04999`] === 5000, "Big test last mutation check failed");
// key_00000 is NOT modified in the last call, so it keeps original value 0
console.assert(lastResult[`key_00000`] === 0, "Big test unmodified key check failed");
// Original should be untouched
console.assert(bigObj[`key_00000`] === 0, "Big test original mutated");
console.assert(bigObj[`key_04999`] === 4999, "Big test original key mutated");
console.log(`Max constraint test: ${totalCalls} produce() calls on ${numKeys}-key obj in ${elapsed}ms`);
// Deeply nested object stress test
let deepObj: Record<string, JSONValue> = { "val": 0 };
for (let i = 0; i < 50; i++) {
deepObj = { "child": deepObj };
}
const deepHelper = new ImmutableHelper(deepObj);
const deepResult = deepHelper.produce(proxy => {
let cur: any = proxy;
for (let i = 0; i < 50; i++) cur = cur.child;
cur.val = 42;
});
// Verify mutation applied
let cur: any = deepResult;
for (let i = 0; i < 50; i++) cur = cur.child;
console.assert(cur.val === 42, "Deep test mutation failed");
// Verify original untouched
let origCur: any = deepObj;
for (let i = 0; i < 50; i++) origCur = origCur.child;
console.assert(origCur.val === 0, "Deep test original mutated");
console.log("All tests passed!");
}
runTests();