The two test cases blow both pass. I simply don't understand the behavior. It seems that JavaScript Proxy cannot trap property getting inside a getter function.
test('JS Proxy normal method', () => {
class Store {
hidden = false;
visible() {
return !this.hidden;
}
}
const accessList: PropertyKey[] = [];
const proxy = new Proxy<Store>(new Store(), {
get: (target: any, propertyKey: PropertyKey) => {
accessList.push(propertyKey);
return Reflect.get(target, propertyKey);
},
});
expect(proxy.visible()).toBe(true);
expect(accessList).toEqual(['visible', 'hidden']);
});
test('JS Proxy getter method', () => {
class Store {
hidden = false;
get visible() {
return !this.hidden;
}
}
const accessList: PropertyKey[] = [];
const proxy = new Proxy<Store>(new Store(), {
get: (target: any, propertyKey: PropertyKey) => {
accessList.push(propertyKey);
return Reflect.get(target, propertyKey);
},
});
expect(proxy.visible).toBe(true);
expect(accessList).toEqual(['visible']);
});
You're missing the receiver of the property access. The property might be defined on a different object than it is accessed on, and your Reflect.get call needs to take that into account. In particular, the receiver you get as a argument of the get trap is the proxy itself, and that's also the object you want to evaluate the getter against, so that its this value refers to the proxy. However, Reflect.get(target, propertyKey) is the same as target[propertyKey], where the this value in the getter is set to the target and the .hidden property access can't be detected by your proxy.
Related
I'm registering some custom elements using customElements.define and would like to automatically set up traps on member accessors so that I can emit events when they change
class State extends HTMLElement {
public someValue = 1;
public constructor() {
super();
console.log('State constructor');
}
}
const oProxy = new Proxy(State, {
get(target, prop: string) {
console.log(`GET trap ${prop}`);
return Reflect.get(target, prop);
},
set(target, prop: string, value: any) {
console.log(`SET trap ${prop}`);
return Reflect.set(target, prop, value);
}
});
customElements.define('my-state', oProxy);
const oStateEl = document.querySelector('my-state');
console.log(oStateEl.someValue);
console.log(oStateEl.someValue = 2);
console.log(oStateEl.someValue);
My browser doesn't seem to have a problem with the above code and I can see some trap output as the element is set up
GET trap prototype
GET trap disabledFeatures
GET trap formAssociated
GET trap prototype
But when I manually get/set values the traps aren't triggered.
Is this even possible?
What I ended up doing was moving all member variable values to a private object and dynamically defining a getter/setter for each as soon as the custom element was mounted on the DOM like so...
//
class State extends HTMLElement {
protected _data: object = {}
public connectedCallback() {
// Loop over member vars
Object.getOwnPropertyNames(this).forEach(sPropertyKey => {
// Ignore private
if(sPropertyKey.startsWith('_')) {
return;
}
// Copy member var to data object
Reflect.set(this._data, sPropertyKey, Reflect.get(this, sPropertyKey));
// Remove member var
Reflect.deleteProperty(this, sPropertyKey);
// Define getter/setter to access data object
Object.defineProperty(this, sPropertyKey, {
set: function(mValue: any) {
console.log(`setting ${sPropertyKey}`);
Reflect.set(this._data, sPropertyKey, mValue);
},
get: function() {
return this._data[sPropertyKey];
}
});
});
}
}
//
class SubState extends State {
public foobar = 'foobar_val';
public flipflop = 'flipflop_val';
public SubStateMethod() { }
}
//
window.customElements.define('sub-state', SubState);
//
const oState = document.querySelector('sub-state') as SubState;
oState.foobar = 'foobar_new_val';
That way I can still get/set values on the object as normal, typescript is happy that the member variables exist, and I can trigger custom events when members are accessed - all while allowing custom elements to exist within the markup at DOM ready
Imagine I have the following code:
const object = {};
// an error should be thrown
object.property.someMethod();
// an error should be thrown
object.foo;
Is it possible to throw an error when someMethod() is called or if any other non-existing property is called?
I guess that I need to do something with it's prototype, to throw an Error. However, I'm not sure what exactly I should do.
Any help would be appreciated.
Yes, using a Proxy with a handler.get() trap:
const object = new Proxy({}, {
get (target, key) {
throw new Error(`attempted access of nonexistent key \`${key}\``);
}
})
object.foo
If you want to modify an existing object with this behavior, you can use Reflect.has() to check for property existence and determine whether to forward the access using Reflect.get() or throw:
const object = new Proxy({
name: 'Fred',
age: 42,
get foo () { return this.bar }
}, {
get (target, key, receiver) {
if (Reflect.has(target, key)) {
return Reflect.get(target, key, receiver)
} else {
throw new Error(`attempted access of nonexistent key \`${key}\``)
}
}
})
console.log(object.name)
console.log(object.age)
console.log(object.foo)
I'm new to the ES6 Proxy object, and am encountering an error I don't understand when attempting to call concat on an array that has been proxied.
Background:
I thought the ES6 Proxy would work perfectly as a way to verify the "purity" of a reducer function in my React/Redux application. I can wrap my state object in a proxy that throws an error if I ever attempt to mutate that object. I'm using something based on the on-change library to do this:
const triggersOnChange = (object, onChange) => {
const handler = {
get (target, property, receiver) {
try {
return new Proxy(target[property], handler)
} catch (err) {
return Reflect.get(target, property, receiver);
}
}
defineProperty (target, property, descriptor) {
onChange()
return Reflect.defineProperty(target, property, descriptor)
}
deleteProperty (target, property) {
onChange()
return Reflect.deleteProperty(target, property)
}
}
return new Proxy(object, handler)
}
And here's an example test of how I intend to use the proxy wrapper:
describe('reducer', () => {
test('it returns an updated state object', () => {
const state = triggersOnChange({ items: [] }, () => {
throw new Error('Oops! You mutated the state object')
})
const action = {
payload: { item: 'foobar' }
}
expect(reducer(state, action)).toEqual({
items: [action.payload.item]
})
})
})
If I implement a "bad" reducer that mutates the state object, my test throws an error as intended:
const reducer = (state, action) => {
state.items.push(action.payload.item) // bad
return state
}
// test throws error "Oops! You mutated the state object"
But when I "purify" my reducer by returning a new state object, I get a different error that I don't quite understand:
const reducer = (state, action) => {
return Object.assign({}, state, {
items: state.items.concat(action.payload.item)
})
}
/*
TypeError: 'get' on proxy: property 'prototype' is a read-only and
non-configurable data property on the proxy target but the proxy did
not return its actual value (expected '[object Array]' but got
'[object Object]')
at Proxy.concat (<anonymous>)
*/
Am I missing something about proxy behavior here? Or is this perhaps an issue with the proxy-chaining behavior that results from my get trap? I initially thought this was a problem with using a proxy within Object.assign, but I encountered the same error when debugging before my reducer's return statement, where I actually use Object.assign. Help!
Edit: Happy to revise this question to make it a little more generic, but I’m not 100% what the issue is so I’ll wait and see if I can get any answers.
Your problem can be replicated with the following code:
var obj = {};
Object.defineProperty(obj, "prop", {
configurable: false,
value: {},
});
var p = new Proxy(obj, {
get(target, property, receiver) {
return new Proxy(Reflect.get(target, property, receiver), {});
},
});
var val = p.prop;
The core of the issue is that objects have invariants that they must stay consistent with, even when accessed via a Proxy object, and in this case you are breaking one of those invariants. If you look at the specification for Proxy's get, it states:
[[Get]] for proxy objects enforces the following invariants:
The value reported for a property must be the same as the value of the corresponding target object property if the target object property is a non-writable, non-configurable own data property.
The value reported for a property must be undefined if the corresponding target object property is a non-configurable own accessor property that has undefined as its [[Get]] attribute.
and in your case, you are not maintaining that first invariant, because even when a property is non-writable and non-configurable, you are returning a wrapping Proxy. The easiest approach would be to ensure that the proper value is returned in that case.
While we're at it, I'll also recommend using typeof explicitly instead of using try/catch so it is clearer.
get(target, property, receiver) {
const desc = Object.getOwnPropertyDescriptor(target, property);
const value = Reflect.get(target, property, receiver);
if (desc && !desc.writable && !desc.configurable) return value;
if (typeof value === "object" && value !== null) return new Proxy(value, handler);
else return value;
},
Here is my code example:
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
class A {
#enumerable(false)
a: number = 1
b: number = 2
myMethod () {}
}
const a = new A()
Whatever I try I get:
D:(real path removed)/first-try-typescript>tsc --emitDecoratorMetadata --experimentalDecorators decorators.ts
decorators.ts(8,3): error TS1240: Unable to resolve signature of property decorator when called as an expression.
I have tried everything from same stackoferflow questions suggestions:
adding emitDecoratorMetadata & experimentalDecorators to tsconfig
running tsc --emitDecoratorMetadata --experimentalDecorators
adding :any to mark decorator function returning value
adding descriptor: TypedPropertyDescriptor<any> type
I always get this error. Both in a terminal and in Webstorm code hints. Method decorator - the same thing (see example below).
function test(target: Object,
propertyKey: string,
descriptor: TypedPropertyDescriptor<any>): any {
return descriptor;
}
class A {
a: number = 1
b: number = 2
#test
myMethod () {}
}
const a = new A()
Up to date code is here - https://github.com/rantiev/first-try-typescript
Unfortunately, property decorators do not have access to the property descriptor, as properties live on the class instance, while decorators are evaluated before any instance could possibly exist. Also, you can only use the following signature for a property decorator:
function (target: any, propKey: string | symbol)
So no descriptor here.
You also can't just do Object.defineProperty(target, propKey.toString, { enumerable: false, value: ... }) because that would be shared across all instances of your class, i.e. setting a property in one instance would leak into another.
Achieving what you are doing is possible though, but a bit complicated. What I generally do is create a getter on the prototype that creates the desired property descriptor just in time. Something like:
function enumerable(value: boolean) {
return function (target: any, propKey: string | symbol) {
Object.defineProperty(target, propKey, {
get: function () {
// In here, 'this' will refer to the class instance.
Object.defineProperty(this, propKey.toString(), {
value: undefined,
enumerable: value
});
},
set: function (setValue: any) {
// In here, 'this' will refer to the class instance.
Object.defineProperty(this, propKey.toString(), {
value: setValue,
enumerable: value
});
}
});
};
}
The "outer" get/set functionality will only run once, as the instance property will shadow it after the property descriptor has been created on the instance.
I'd like to create a decorator that can be applied to methods,
It's goal is to control whether you're allowed to run a certain method or not.
Meaning it should have a certain condition, if it passes it'll run as usual (in the same context as well)
Here's a shot I took on this but failed due to private members the object has, and now had no access to when I ran the function:
return function(target:any, propertyKey: string, descriptor: PropertyDescriptor){
var funcToRun = descriptor.value;
descriptor.value = () => {
if(true) { //if has permissions
return p.call(target);
}
}
}
Thanks in advance.
I wouldn't change the passed descriptor but instead would return a changed copy.
Here's a working version of what you asked for:
function deco(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const newDescriptor = Object.assign({}, descriptor);
newDescriptor.value = function () {
if (this.x > 0) {
return descriptor.value.apply(this, arguments);
} else {
throw new Error(`can't invoke ${ propertyKey }`);
}
}
return newDescriptor;
}
class A {
constructor(private x: number) {}
#deco
methodA() {
console.log("A.methodA");
}
}
let a1 = new A(10);
a1.methodA(); // prints: "A.methodA"
let a2 = new A(-10);
a1.methodA(); // throws error
(code in playground)