JS Proxy Shenanigans
This is a quick walk through all the things that might be unexpected while dealing with the Proxy standard as presented, and proudly so, by ECMAScript.
The non-existent typeof expectation
By design, proxies can deal only with non-primitive values, except function and objects. However, there’s a hidden quite-primitive value to deal with, which is a non typed Array, as typed work as expected, and this is why:
const wut = new Proxy([], {
ownKeys: () => []
});
Object.keys(wut);
// Uncaught TypeError:
// 'ownKeys' on proxy: trap result
// did not include 'length'
“… and what’s the issue there?” well, the issue is that a generic proxy handler should account for typeof target === “object” && target !== null && Array.isArray(target)
within the handler to eventually return non-breaking runtime code … how user friendly is that?
The non-existent isArray trap
Following the previous example, the shenanigans are not only on array references ’cause, if you wrap your array or value into an object, other things will go wrong:
const wut = new Proxy({value: [1, 2, 3]}, Reflect);
Array.isArray(wut);
// false
The issue here is that if we’d like to pretend any non array value is an array, we’ll fail, but when we actually use an array as trap, we won’t survive the isArray
check as that would be true
… so that using arrays as unique way to deal with Proxies is also not a good idea, as I’ve recently learned in one of my most complex projects to date.
That project also needed to monkey patch the global isArray
to provide reliable results as it’s impossible to have just a typeof "object"
in the mix and make all traps and native calls behavior be good with each other.
Apply and Construct want functions
This is an easy one to maybe understand: if your proxied value is not a function, apply
or construct
traps won’t work:
const wut = new Proxy({}, {
apply() {},
construct() {}
});
new wut();
// Uncaught TypeError:
// wut is not a constructor
TC39 members would die on the hill that justifies this behavior, but to me everything I’ve encountered so far is just an odd spec based on the target your are proxying instead of one based on the traps you are providing … I mean: why doesn’t that new Proxy
operation throws already if it encounters traps it’s not meant to handle, instead of breaking potentially production code out of the blue and too late on the pipe? 🤦 🤷
I can hear the “because of generic handlers meant to deal with all cases” as excuse in there but then again, surprises are the only things that land in production to me. It’s trivial enough to define ad-hoc handlers for typeof "function"
VS handlers for typeof "object"
in our daily code, this was a pure “delegate to user-land” shenanigan to me.
The Proxy I Dream About
After dealing with Python long enough to appreciate all its magic handlers about all proxied things, I don’t want JS to strictly mimic all of that but I feel like a better Proxy primitive is missing in the language:
- the Proxy premise is that nobody should understand if a reference, not a primitive, is a proxy or not … yet
instanceof
opertaor, amongArray.isArray
and bothapply
andconstructor
traps, leak the original intent of a value … so that all of these shenanigans are acceptable to TC39:(Array.isArray(p) && !(p instanceof Array)
orObject.keys(p).length === 10 && p.length === undefined
ortypeof p === 'function' && p() /* throw a non callable error */
… all of this is fine and perfect for TC39, but by no mean acceptable for developers … strawberry on top, it’s impossible to proxy DOM nodes on the Web and pretend these will work out of the blue as every single API based on JS drills the “non discoverable” Proxy nature and respectfully fucks everything up on demand - the current Proxy API is screaming for a disambiguation trap … as the
instanceof
dedicated one has not been internally used to break out of the blue for traps meant to simply carry developers intended values … theinstanceof
trap fails to disambiguate for functions, arrays, or anything else, that’s used internally in the proxy resolution steps .. how bizarre … they care about not revealing yet there are holes in that specs that reveal all sort of issues … - the Proxy I am dreaming about does not relate anything to the target and it simply acts like a proxy so that the membrane is really transparent but we can intercept any foreign operation to that membrane so that
ownKeys
returns just the keys we want to return,apply
orconstruct
just pass to the proxy traps or fail if these are absent, and so on for any other available trap in the specs.
That’s it … Proxy is half-doomed while opening tons of interesing, not purposely designed, use cases these days, but they lack a lot of real-world scenarios, developers intents, and expectations, so I hope this post helped you at least a bit.