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
apply
andconstruct
traps won't work with it. If the object somehow wants to represent an array without being one, it's impossible to surviveArray.isArray
brand check (it will befalse
) and withownKeys
the target needs to have a non configurablelength
property or it will also fails once reached - array: it's like object but it survives the
Array.isArray
andReflect.ownKeys
checks but it cannot represent also objects becauseownKeys
requires an expected non configurablelength
property andArray.isArray
will always returntrue
no matter what - function: it's possible to intercept almost all traps by proxying a function and the
typeof
on 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. Theconstruct
trap 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 proxy
brand check which returns function ⚠️ while non constructable functions throw withnew
without ever invoking theconstruct
trap, classes will not throw onapply
out of the box, so you are in charge of eventually handling a class that cannot be invoked withoutnew
maybe 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
apply
if 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
length
property 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 function
and handle with care theapply
trap 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.