-1

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

valepu
  • 3,136
  • 7
  • 36
  • 67
  • I think you mean "proxied". – T.J. Crowder Dec 01 '21 at 10:40
  • What is the underlying purpose to this class? It seems like whatever would use it would be better served by a `Map`. Knowing what you want it for might help us help you solve the problem. – T.J. Crowder Dec 01 '21 at 10:46
  • @T.J.Crowder at the moment i'm not using proxies in any pratical way, I was just testing how they work and ended up with this issue. Also feel free to edit my question if i wrote the word "proxed" wrong (English is not my main language, sorry) – valepu Dec 01 '21 at 12:28
  • I don't understand `new Proxy(this, this)`. Generally speaking, the *handlers* are not the same object as the *target* and who knows what crazy stuff could happen if they are? Like, is it possible to modify the handlers by changing the `get` and `set` properties of the target? I hope not, but that really depends on how the `Proxy` constructor is implemented, right? Or maybe the spec rues that out, but again, why would anyone want to have to explain that? Normally I'd say that the TS typings come from the JS use cases, but I don't get the JS use case here at all. – jcalz Dec 01 '21 at 15:41
  • @jcalz I found that way of using a proxy here: https://github.com/Microsoft/TypeScript/issues/28776 the use case is "i want to have an object i can call any property name on without getting an error while using trypescript" i mean this seems to be a very standard use case for Proxy – valepu Dec 01 '21 at 16:27
  • The use case might be "very standard" but `new Proxy(this, this)` is not. I see [this answer](https://stackoverflow.com/a/53195487/2887218) and [this question](https://stackoverflow.com/q/64669523/2887218) and a few other places when I search, but nothing that would really recommend this approach. – jcalz Dec 01 '21 at 16:38
  • Since the approach for creating a proxy is not part of my question i edited it so that it uses a more standard one – valepu Dec 01 '21 at 16:50
  • So, I might give up on trying to get the compiler to infer the type of `ProxTest` from its declaration and just assert like [this](https://tsplay.dev/mbGybW). If that works for you I could write up an answer, but I don't understand the purpose of that `a` property. Even `storage` wasn't being used but at least it's easily implemented. Could you clear that up for me and let me know if you want an answer? – jcalz Dec 01 '21 at 18:01
  • just there as an example of a variable that gives error when adding index signature. As I said i'm playing around with proxieswhen this issue occurred – valepu Dec 01 '21 at 18:40
  • Well, the `a` will be useless, but ‍♂️. Do you want me to write up my suggested code solution as an answer with an explanation? Or are you looking for something else? – jcalz Dec 01 '21 at 18:44
  • Probably my question is not clear i'm afraid. My issue is that when using Proxy in typescript to have an instantiated object of any kind, with an arbitrarily diverse number of variables, be able to handle unknown properties (one of Proxy use cases) I have a compile error, i'd like a solution that works in **every** case (because imho it doesn't make sense that I can't use Proxy for one of its use cases). I found a solution (using index signatures and adding ts-ignore to other properties) but it's janky. I'll edit my question to make this clear – valepu Dec 01 '21 at 19:14
  • If your solution covers the general issue I'll gladly accept it and I'll gladly enjoy an explaination – valepu Dec 01 '21 at 19:15
  • The problem isn't the `Proxy` but the `class` declaration. It's not really very common to return anything from a `class` constructor method, which leads to weird behavior (like generally `new Foo() instanceof Foo` will evaluate to `false`). – jcalz Dec 01 '21 at 19:19
  • if I do `const c = new Proxy(new ProxTest(), handler);` it will make `c` of type `any`, which, while it works, is not really nice since I lose ProxTest's type and have no code suggestions. If i do `const c = new Proxy(new ProxTest(), handler);` or `const c: ProxTest = new Proxy(new ProxTest(), handler);` then the issue will be back again – valepu Dec 01 '21 at 19:28
  • I need to see the problem you're referring to as a [mre] or even better as a link to the TS Playground (you can use https://tsplay.dev to shorten such links in comments) or I won't be able to address it. If you really care about using a `class` my suggestion would be type assertions and something like `ProxTestConstructor` because the compiler does not understand what happens to the instance type if you return something from the constructor. If you don't care about a `class` then you should refactor your code example to show whatever the issue is with `new Proxy(new ProxTest(), handler)`. – jcalz Dec 01 '21 at 19:31
  • But in either case, since returning something from a class constructor and writing an arbitrary `Proxy` handler are both potentially doing arbitrary things with types, you will probably need type assertions to proceed. I'm not sure if you're saying that you think the compiler should just understand what you intend to do with your proxies and automatically give it a string index signature of `any` property values, but unfortunately the compiler is not as smart as you. It assumes the proxy is basically the same type as the proxied target. – jcalz Dec 01 '21 at 19:34
  • 1
    Maybe you just want a link to https://github.com/microsoft/TypeScript/issues/20846 ? – jcalz Dec 01 '21 at 19:36
  • 1
    Let me know if any of this looks like answer material to you. I don't know if anything I've said helps with "the general issue" because I'm not sure I really understand what issue that is. If it's just that TypeScript thinks `new Proxy(x, handler)` is the same type as `x` then that's a limitation of TypeScript and we can only work around it with type assertions or `//@ts-ignore` (not recommended though) or the like – jcalz Dec 01 '21 at 19:39
  • I'll check the link and try to make an example but I think now I got where the misunderstanding is and it's my fault: when having a Proxied object the get/set trap will trigger for every variable not just unkown ones (even tho you can put a check on the getter/setter for existing variable, it will still handle every situation) and obviously typescript can't know what you are doing with the get/set. I thought existing variables would still be accessed normally while only unkown ones by the get/set (a bit like PHP magic methods) so yeah i think this is the answer: I didn't understand proxies :) – valepu Dec 01 '21 at 20:03

1 Answers1

0

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 :)

valepu
  • 3,136
  • 7
  • 36
  • 67