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?
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.