Problem

Write a function that takes an object obj and returns a new immutable version of this object.

An **immutable **object is an object that can’t be altered and will throw an error if any attempt is made to alter it.

There are three types of error messages that can be produced from this new object.

  • Attempting to modify a key on the object will result in this error message: Error Modifying: ${key}.
  • Attempting to modify an index on an array will result in this error message: Error Modifying Index: ${index}.
  • Attempting to call a method that mutates an array will result in this error message: Error Calling Method: ${methodName}. You may assume the only methods that can mutate an array are ['pop', 'push', 'shift', 'unshift', 'splice', 'sort', 'reverse'].

obj is a valid JSON object or array, meaning it is the output of JSON.parse().

Note that a string literal should be thrown, not an Error.

Examples

Example 1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Input: 
obj = {
"x": 5
}
fn = (obj) => { 
obj.x = 5;
return obj.x;
}
Output: {"value": null, "error": "Error Modifying: x"}
Explanation: Attempting to modify a key on an object resuts in a thrown error. Note that it doesn't matter that the value was set to the same value as it was before.

Example 2:

1
2
3
4
5
6
7
8
Input: 
obj = [1, 2, 3]
fn = (arr) => { 
arr[1] = {}; 
return arr[2]; 
}
Output: {"value": null, "error": "Error Modifying Index: 1"}
Explanation: Attempting to modify an array results in a thrown error.

Example 3:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Input: 
obj = {
"arr": [1, 2, 3]
}
fn = (obj) => { 
obj.arr.push(4);
return 42;
}
Output: { "value": null, "error": "Error Calling Method: push"}
Explanation: Calling a method that can result in a mutation results in a thrown error.

Example 4:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Input: 
obj = {
"x": 2,
"y": 2
}
fn = (obj) => { 
return Object.keys(obj);
}
Output: {"value": ["x", "y"], "error": null}
Explanation: No mutations were attempted so the function returns as normal.

Constraints:

  • obj is a valid JSON object or array
  • 2 <= JSON.stringify(obj).length <= 10^5

Solution

Method 1 – Proxy and Recursion

Intuition

To make an object deeply immutable, we can use JavaScript’s Proxy to intercept all mutation attempts. For objects and arrays, we recursively wrap their properties or elements. For arrays, we also intercept mutating methods and throw the required error messages.

Approach

  1. Define a set of mutating array methods: ['pop', 'push', 'shift', 'unshift', 'splice', 'sort', 'reverse'].
  2. Write a recursive function that returns a Proxy for the input object or array.
  3. In the Proxy handler:
    • For set (property assignment), throw an error with the appropriate message for objects or arrays.
    • For deleteProperty, throw an error for deletion attempts.
    • For arrays, intercept mutating method calls and throw an error.
    • For property access, recursively wrap nested objects/arrays.
  4. Return the Proxy-wrapped object.

Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function makeImmutable(obj) {
    const mutatingMethods = ['pop', 'push', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
    function wrap(target) {
        if (typeof target !== 'object' || target === null) return target;
        return new Proxy(target, {
            set(t, key, value) {
                if (Array.isArray(t)) {
                    throw `Error Modifying Index: ${key}`;
                } else {
                    throw `Error Modifying: ${key}`;
                }
            },
            deleteProperty(t, key) {
                if (Array.isArray(t)) {
                    throw `Error Modifying Index: ${key}`;
                } else {
                    throw `Error Modifying: ${key}`;
                }
            },
            get(t, key, receiver) {
                if (Array.isArray(t) && mutatingMethods.includes(key)) {
                    return function() { throw `Error Calling Method: ${key}`; };
                }
                const val = Reflect.get(t, key, receiver);
                return wrap(val);
            }
        });
    }
    return wrap(obj);
}

Complexity

  • ⏰ Time complexity: O(n), where n is the number of properties/elements, as each is wrapped once.
  • 🧺 Space complexity: O(n), for the proxies and recursion stack.