Skip to content

Instantly share code, notes, and snippets.

@WebReflection
Last active November 23, 2023 19:35
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save WebReflection/58ce94a0d2c4118a4f3f26934c9582f4 to your computer and use it in GitHub Desktop.
Save WebReflection/58ce94a0d2c4118a4f3f26934c9582f4 to your computer and use it in GitHub Desktop.
Proxy Traps Cheat Sheet

Proxy Traps Cheat Sheet

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.

The 3 + 1 Proxy Types

  • object: any non primitive value can be proxied but apply and construct traps won't work with it. If the object somehow wants to represent an array without being one, it's impossible to survive Array.isArray brand check (it will be false) and with ownKeys the target needs to have a non configurable length property or it will also fails once reached
  • array: it's like object but it survives the Array.isArray and Reflect.ownKeys checks but it cannot represent also objects because ownKeys requires an expected non configurable length property and Array.isArray will always return true 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 when new proxied() is attempted. The construct trap won't even be reached and an error will be thrown instead
    • constructable: it's either a class {} or a good old function () {}, no matter if named, strict or not. A constructurable proxy target passes through all the traps but it will always reveal its nature via typeof proxy brand check which returns function
    • ⚠️ while non constructable functions throw with new without ever invoking the construct trap, classes will not throw on apply out of the box, so you are in charge of eventually handling a class that cannot be invoked without new maybe before trying to invoke it via new

ℹ️ How to know if a function is constructable?

/**
 * 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')
);

Traps VS Proxy Types

object array non constructable constructable
apply ⚠️ 1.
construct
defineProperty
deleteProperty
get
getOwnPropertyDescriptor ⚠️ 2.
getPrototypeOf
has
isExtensible
ownKeys ⚠️ 3.
preventExtensions
set
setPrototypeOf
Array.isArray
typeof object object function function
  1. it will throw an error within the apply if the target is meant to be created via new target() and not just target()
  2. if the target is used to wrap something else the Proxy expect this something else to return descriptors similar to an array
  3. if the target is used to wrap something else a length property is expected to be present

Safe Indirections

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 the apply trap for classes not meant to be initialized without new

ℹ️ What kind of function should I use?

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment