In an effort to learn how Proxy works I wanted to try and make a Proxied class that can handle any property name, even those that don't exist in the class, while still maintaining access to old properties. I assumed Typescript would let you do this natively (since this is one of Proxy use cases after all) when a class is Proxied but I sadly discovered this is not the case. When i try to do something like this:
const handler = {
get: (target: any, key: string) => {
return key;
},
set: (target: any, key: string, value: any) => {
console.log(target, key, value);
return true;
}
};
class ProxTest {
private storage: {[key: string]: any} = {};
public a: number;
constructor() {
return new Proxy(this, handler);
}
}
const c = new ProxTest();
c.val = "b"; //Gives error
I get an error trying to access c.val
. I have found a "solution", that's to use "Index Signatures" and add //@ts-ignore to all the other properties.
const handler = {
get: (target: any, key: string) => {
return key;
},
set: (target: any, key: string, value: any) => {
console.log(target, key, value);
return true;
}
};
class ProxTest {
//@ts-ignore
private storage: {[key: string]: any} = {};
//@ts-ignore
public a: number;
[key: string]: any;
constructor() {
return new Proxy(this, handler);
}
}
const c = new ProxTest();
c.val = "b";
This is the only solution I found so far and to be honest I don't really like it. Is there some other way to do this?
To be clear, this code is just an example of the general issue (not being able to use arbitrarily named properties on a proxied object) and it's in no way shape or form related to some code i'm actually using. But since this seems something one should be able to do when using Proxy in typescript i'd like to find a general solution
So, as per the comments I had misunderstood how Proxy worked and I thought it worked like PHP magic methods (that is, that the get/set trap got triggered only when trying to access unkown properties), instead it works for all the properties, even those that already exist in the object.
I assumed that typescript would automatically detect a proxied class and let you use the normal values of the class as before it was proxied while it would just not give an error when trying to access unkown properties and this is probably why I couldn't make myself clear about what I was asking.
By default, when using Proxy, typescript will change the type to "any", this of course lets you use any kind of property on the object without having an error, at the cost of losing direct access to the already existing properties (this is actually in line with javascript because when you Proxy a class and declare get/set everything goes through those methods)
So, what I really wanted to achieve was to have a proxied class where I could still access the already existing properties and where typescript would correctly infer the type of the already existing properties, while still not having an error when trying to access non existing ones. I came up with this solution:
interface ProxTestStorage extends ProxTestConstructor {
[key: string]: any;
}
interface ProxTestConstructor {
new(): ProxTestImpl & ProxTestStorage;
}
class ProxTestImpl {
protected storage: {[key: string]: any} = {};
public a: number = 3;
}
const handler = {
get: (target: any, key: string) => {
console.log(target, key);
return Reflect.get(key in target ? target : target.storage, key);
},
set: (target: any, key: string, value: any) => {
console.log(target, key);
return Reflect.set(key in target ? target : target.storage, key, value);
}
};
const ProxTest = ProxTestImpl as any as ProxTestImpl & ProxTestStorage;
const c = new Proxy<typeof ProxTest>(new ProxTest(), handler);
c.val = "b"; //No error, type is any
c.a = 2; //No error, type is number
console.log(c); // ...{a: 2, storage: {val: "b"}}...
Of course get and set need to be implemented properly to let you access existing properties (using "in" has the drawback that the property needs to be initialized to be seen, but since this is "just" an exercise to understand Proxies I'm ok with it) and they have to be coherent with your type's logic. By declaring the type like this I have to use "typeof ProxTest" when using ProxTest as a type (unlike with normal classes) but I guess it's a minor issue now and it goes beyond the scope of my question.
Thanks @jcalz for the patience :)