Preventing a Deeply Nested Object From Being Mutated

Last reviewed on November 13, 2021

Recently I was trying to figure out how to prevent a deeply nested object from being mutated. That is, I didn't want properties in the object being changed, added, or deleted. Let's use this object as an example:

const object = {
  foo: 'foo',
  bar: {
    baz: 'baz',
    qux: {
      fred: 'fred',
    },
  },
};

My first thought was to use Object.freeze. However, this doesn't work for nested objects. One solution was to write a function that recursively deep freezes an object.

Instead, I decided to give the Proxy class a try and was able to achieve similar behavior:

const handler = {
  set(target, property, value, receiver) {
    throw new Error(`Can't assign "${property}" on this Proxy object`);
  },

  get(target, property, receiver) {
    if (typeof target[property] === 'object') {
      return new Proxy(target[property], handler);
    }

    return Reflect.get(...arguments);
  },
};

const proxy = new Proxy(object, handler);

With the above, I can access the nested properties of object as usual:

proxy.foo; // 'foo'
proxy.bar.baz; // 'baz'
proxy.bar.qux.fred; // 'fred'

And if I try to update any of the nested properties, an error is thrown:

proxy.foo = 'updated';
// Error: Can't assign "foo" on this Proxy object

proxy.bar.baz = 'updated';
// Error: Can't assign "baz" on this Proxy object

proxy.bar.qux.fred = 'updated';
// Error: Can't assign "fred" on this Proxy object

Now a little more explantation. In the code above, the get method of handler is called when attempting to access immediate properties in object. If one of those properties is an object, I return a new Proxy for that nested object with the same behavior. Otherwise, I return the value for that property via Refject.get. This is like a lazily deep frozen object.

Whenever a property is attempted to be set on the object through the proxy, set is called, and I just throw an error and the property is never assigned.