Search code examples
javascriptnode.jses6-proxy

TypeError with Proxy class - TypeError: 'set' on proxy: trap returned truish for property


I am getting this fun error when using the Proxy class:

TypeError: 'set' on proxy: trap returned truish for property 'users' which exists in the proxy target as a non-configurable and non-writable data property with a different value

I have a library which creates proxy object properties recursively, where any non-primitive property is a Proxy object itself, etc etc.:

let mcProxy = function (target) {
  const mirrorCache = {};
  return new Proxy(target, {
    set: function (target, property, value, receiver) {
       if (mirrorCache[property]) {
          throw new Error(`property ${property} has already been set`);
       }
        mirrorCache[property] = true;
        Object.defineProperty(target, property, {
          writable: false,
          value: (value && typeof value === 'object') ? mcProxy(value) : value
        });
        return true;
    }
  });
};

exports.create = function (val) {
  val && assert.equal(typeof val, 'object', 'val must be an object');
  return mcProxy(val || {});
};

actual usage of the above library code:

//bash
$ npm install proxy-mcproxy

 // nodejs
 let McProxy = require('proxy-mcproxy');
 let val = McProxy.create();
 val.users = [];
 val.users = 3; // kaaaboom..error!

but when I set the users property the first time, I get the error in title of this question!

In my library code above, mirrorCache is a way to check if the property has been previously set. Want I want to do, is to throw an error, even if we aren't in strict mode, so mirrorCache appears to be necessary so that I do my own bookkeeping.

Perhaps there is a different or better way to achieve what I want to achieve? Here are my goals:

  1. Throw an error even if not in strict mode.
  2. Throw an error anytime the developer re-assigns a property. Each assigned property should be immutable.

Solution

  • Take a look at the following, section 9.5.9 of the ECMA spec:

    http://www.ecma-international.org/ecma-262/6.0/#sec-proxy-object-internal-methods-and-internal-slots-set-p-v-receiver

    A riveting read I'm sure you'll agree.

    I believe the two keys lines are:

    1. Let booleanTrapResult be ToBoolean(Call(trap, handler, «target, P, V, Receiver»)).

    and the equally esoteric:

    1. If targetDesc is not undefined, then

      a. If IsDataDescriptor(targetDesc) and targetDesc.[[Configurable]] is false and targetDesc.[[Writable]] is false, then

      i. If SameValue(V, targetDesc.[[Value]]) is false, throw a TypeError exception.

    There is this relevant comment in the NOTE section:

    Cannot change the value of a property to be different from the value of the corresponding target object property if the corresponding target object property is a non-writable, non-configurable own data property.

    That note tries to put it into English but it doesn't indicate the key detail, which is the timing of the steps. Point 9 is the bit where your setter (trap) gets called. Unfortunately the bit where it checks whether the property is writable is point 14. So by the time the check is performed the property is indeed non-writable and non-configurable.

    One way to fix this is to make the property configurable by chucking in a configurable: true in your defineProperty. I don't entirely follow your use case so I can't tell whether that would be an acceptable compromise.

    I'm also wondering why you need to set these properties to be non-writable in the first place. If the underlying objects will always be accessed via their proxies then you have total control over all the set calls. I'm not even really sure why you need the mirrorCache rather than just checking whether the property is already in the target object. If you can't assume that the objects will always be accessed via their proxies then it would seem you've already lost the battle as properties can be changed without you ever knowing a thing about it.

    Something like this seems close to what you want:

    let mcProxy = function (target) {
      return new Proxy(target, {
        set: function (target, property, value) {
          if (Object.prototype.hasOwnProperty.call(target, property)) {
            throw new Error(`property ${property} has already been set`);
          }
    
          target[property] = (value && typeof value === 'object') ? mcProxy(value) : value;
    
          return true;
        }
      });
    };
    

    It needs a bit more tweaking to work with arrays properly but I'm unclear which array methods you would expect to support.