Search code examples
javascriptes6-proxy

JavaScript Proxy target object conflicts with property descriptors


I'm creating a virtualized object that lazily builds its properties with a rules engine as a technique to skip calculating values that are never read. For this, I'm using a Proxy. It seems like proxies sort of serve double-duty for both access forwarding and virtualizing; in my case, I mostly care about the latter, not the former.

The problem I'm having has to do with trying to implement the getOwnPropertyDescriptor trap in the proxy. When doing so I get the error:

TypeError: 'getOwnPropertyDescriptor' on proxy: trap returned descriptor for property 'foo' that is incompatible with the existing property in the proxy target

Because I'm not actually forwarding requests to a wrapped object, I've been using an Object.freeze({}) as the first argument passed to new Proxy(...).

function createLazyRuleEvaluator(rulesEngine, settings) {
  const dummyObj = Object.freeze({});
  Object.preventExtensions(dummyObj);

  const valueCache = new Map();

  const handlers = {
    get(baseValue, prop, target) {
      if (valueCache.has(prop)) 
        return valueCache.get(prop);

      const value = rulesEngine.resolveValue(settings, prop);
      valueCache.set(prop, value);
      return value;
    },

    getOwnPropertyDescriptor(baseValue, prop) {
      return !propIsInSettings(prop, settings) ? undefined : {
        configurable: false,
        enumerable: true,
        get() {
          return handlers.get(baseValue, prop, null);
        }
      };
    },
  };

  return new Proxy(dummyObj, handlers);
}

// This throws
Object.getOwnPropertyDescriptor(createLazyRuleEvaluator(/*...*/), 'foo');

The proxy object is meant to resemble a object with read-only properties that can't be extended or have any other such fanciness. I tried using a frozen and non-extensible object, but I'm still told the property descriptor is incompatible.

When I try using a non-frozen object as the proxy target, then I get a different type error:

TypeError: 'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property 'foo' which is either non-existent or configurable in the proxy target

What am I doing wrong here? Is there any way I can have my proxy exhibit non-configurability?


Solution

  • You might want to consult the list of invariants for handler.getOwnPropertyDescriptor(), which unfortunately includes these:

    • A property cannot be reported as existent, if it does not exist as an own property of the target object and the target object is not extensible.
    • A property cannot be reported as non-configurable, if it does not exist as an own property of the target object or if it exists as a configurable own property of the target object.

    Since your target is a frozen, empty object, the only valid return value for your handler.getOwnPropertyDescriptor() trap is undefined.


    To address the underlying question though, lazily initialize your property descriptors when they're attempted to be accessed:

    function createLazyRuleEvaluator(rulesEngine, settings) {
      const dummyObj = Object.create(null);
      const valueCache = new Map();
      const handlers = {
        get(target, prop) {
          if (valueCache.has(prop)) 
            return valueCache.get(prop);
    
          const value = prop + '_value'; // rulesEngine.resolveValue(settings, prop)
          valueCache.set(prop, value);
          return value;
        },
        getOwnPropertyDescriptor(target, prop) {
          const descriptor =
            Reflect.getOwnPropertyDescriptor(target, prop) ||
            { value: handlers.get(target, prop) };
    
          Object.defineProperty(target, prop, descriptor);
          return descriptor;
        },
      };
    
      return new Proxy(dummyObj, handlers);
    }
    
    console.log(Object.getOwnPropertyDescriptor(createLazyRuleEvaluator(/*...*/), 'foo'));

    You can follow this pattern for each of the method traps to preempt unwanted mutations to your dummyObj by lazily initializing any properties accessed.