There are various shenanigans around the Proxy API, including issues with Array.isArray and Object.ownKeys so that this gits purpose is to describe all the undocummented caveats to help anyone dealing with all possibilities this half-doomed API offers.
- object: any non primitive value can be proxied but
applyandconstructtraps won't work with it. If the object somehow wants to represent an array without being one, it's impossible to surviveArray.isArraybrand check (it will befalse) and withownKeysthe target needs to have a non configurablelengthproperty or it will also fails once reached - array: it's like object but it survives the
Array.isArrayandReflect.ownKeyschecks but it cannot represent also objects becauseownKeysrequires an expected non configurablelengthproperty andArray.isArraywill always returntrueno matter what - function: it's possible to intercept almost all traps by proxying a function and the
typeofon that proxy will always return function. Moreover, there are two kind of functions in JS:- non constructable: it's an arrow function (
() => {}) or a method one ({method(){}}). Thes kind of functions pass through all traps except they fail ASAP whennew proxied()is attempted. Theconstructtrap won't even be reached and an error will be thrown instead - constructable: it's either a
class {}or a good oldfunction () {}, no matter if named, strict or not. A constructurable proxy target passes through all the traps but it will always reveal its nature viatypeof proxybrand check which returns function ⚠️ while non constructable functions throw withnewwithout ever invoking theconstructtrap, classes will not throw onapplyout of the box, so you are in charge of eventually handling a class that cannot be invoked withoutnewmaybe before trying to invoke it vianew
- non constructable: it's an arrow function (
/**
* Return `true` if the `value` is a function and it
* can be used to create instances via `new value()`
* @param {any} value
* @returns {boolean}
*/
const isConstructable = value => (
typeof value === 'function' &&
Object.hasOwn(value, 'prototype')
);| object | array | non constructable | constructable | |
|---|---|---|---|---|
| apply | ✔ | |||
| construct | ✔ | |||
| defineProperty | ✔ | ✔ | ✔ | ✔ |
| deleteProperty | ✔ | ✔ | ✔ | ✔ |
| get | ✔ | ✔ | ✔ | ✔ |
| getOwnPropertyDescriptor | ✔ | ✔ | ✔ | |
| getPrototypeOf | ✔ | ✔ | ✔ | ✔ |
| has | ✔ | ✔ | ✔ | ✔ |
| isExtensible | ✔ | ✔ | ✔ | ✔ |
| ownKeys | ✔ | ✔ | ✔ | |
| preventExtensions | ✔ | ✔ | ✔ | ✔ |
| set | ✔ | ✔ | ✔ | ✔ |
| setPrototypeOf | ✔ | ✔ | ✔ | ✔ |
| Array.isArray | ✔ | |||
| typeof | object | object | function | function |
- it will throw an error within the
applyif the target is meant to be created vianew target()and not justtarget() - if the target is used to wrap something else the Proxy expect this something else to return descriptors similar to an array
- if the target is used to wrap something else a
lengthproperty is expected to be present
Accordingly with the current state of affairs there are no workarounds to preserve typeof and Array.isArray among other internal checks and operations if not by:
- use an object to trap everything that is not an array or a function
- use an array to trap anything that is actually an array. Note that typed arrays should not be trapped as array as these are recognized and handled like any other object
- use a generic function to trap any
typeof functionand handle with care theapplytrap for classes not meant to be initialized withoutnew
function Wrap() {
'use strict';
return this;
}The 'use strict'; is needed to eventually bind even primitives and not just references.
With Wrap.bind(anyValue) we are sure the reference is handled as function and both apply and construct traps will work as expected.
What is bound can always be retrieved via a wrap() call so that internally this function could store foreign related stuff or whatnot.
This utility is provided by proxy-target as bound and unbound export.
proxy-target module also simplify a lot dealing with all these shenanigans so that, when in doubt, don't hesitate to use that instead of reinventing the same wheel all over.
The most advanced use case for remote, cross realm, cross interpreter, and client server traps based project I've made is called coincident and it's based on proxy-target and it's working very well in production already.