The Array.isArray shenanigans

I have a library (not worth a mention for this thread sake) that forward any Proxy trap to another realm and it was my surprise to realize that Array.isArray(ref) returns always true or always false if that Proxied value is either an array or an object.

As a result, I had to monkey patch Array.isArray in the thread global scope because the proxy simply forward information to other threads (the main in this case) but even if these are new Number(123) and all details to forward such information is there, the Array.isArray(Proxy(Number)) would return true because the proxy uses a type / value tuple to define that reference ... let's say ['number', 1] for discussion sake.

Missing

There is no trap or Reflect method to decide when an Array.isArray(ref) brand check happens, but such method infer the type out of the blue.

If I use an object {type, value} instead as proxied value, any brand check for Array will return false even if the referenced value in my thread is an array.

This has caused a lot of issues in using all proxy traps that survive my code but won't survive 3rd party libraries using Array.isArray(ref) to do their logic, so that at the end a (not so) obtrusive monkey-patch of the native function in the thread my code works into too was needed to fix 3rd party libraries just using plain JS and fix my library behavior around these 3rd party libraries expectations.

Can anyone please understand what I am saying and tell me why any "exotic proxy reference" should just return true when the whole point of proxies is that you should never know what you deal with, until you really deal with it?

Thank you.

A Proxy instance behaves like it's target. Array.isArray will return the value as if applied to the target. Similarly, typeof proxy === 'function' if the target is a callable, which you cannot runtime patch.

If you want your proxy instance to behave like aplain object, just use a plain object as the target. The target object does not need to be the same as the object in the other realm onto which you apply the traps. Do note however that the object invariants must be fulfilled on the target, so for example if isExtensible returns false, the target object must be non extensible.

not at all ... typeof [] is "object" so that was indeed a very bad example and (imho) it underlines my point further ... beside typeof that can't indeed be mimicked via a Proxy, why there's no way to define the result of native Array.isArray out there?

I can mimic all other new Boolean or similar wrappers, I can't tell a Proxy the held value is not an array ...

Maybe I wasn't clear ... I want my proxy "whatever" to behave like "whatever" is held ... I can have numbers, boolean, TypedArray, string references (as objects) and there's no way I can use the same common "struct" to represent them all ... for function I can bind their "struct" into a real function but typeof indeed give me "object" for all other wrappers, the rest is primitive so it works just fine (out of a proxy).

if adding anything, Array.isArray over a non-meant-to-be arrays is a detail leak the Proxy offers today to side-channel libraries that would like to use either a [type, value] underlying reference, or a {type. value} one ... so this somehow concerns me further, as both are typeof "object" but isArray current check will reveal their real nature, while any other usage of that proxied value wouldn't (instanceof to start with).

const thing = new Proxy([], {getPrototypeOf: () => Object.prototype});
if (
  typeof thing === 'object' &&
  Array.isArray(thing) &&
  !(thing instanceof Array)
) {
  console.log('what the heck is this?');
}

Proxy isn’t a generic replacement for every kind of thing - it won’t work with things that have private fields or internal slots either.

I rise this example one more time ...

const thing = new Proxy([], {getPrototypeOf: () => Object.prototype});
if (
  typeof thing === 'object' &&
  Array.isArray(thing) &&
  !(thing instanceof Array)
) {
  console.log('what the heck is this?');
}

how is anyone out there able to understand what a proxied value is when all the odds are in place?

You don't even need a proxy to get a what-the-heck object; in general, any branded-check is orthogonal to instanceof, because the former uses a private property.

const thing = [];
Object.setPrototypeOf(thing, Object.prototype);
if (
  typeof thing === 'object' &&
  Array.isArray(thing) &&
  !(thing instanceof Array)
) {
  console.log('what the heck is this?');
}

However, I believe Array.isArray drilling through proxies is indeed a special case.

1 Like

or is it? how is any non Array object able to satisfy a primitive / widely used check, as Array.isArray(ref) is?

Not only the proxy drilling is concerning, without any trap to tell otherwise, the Array.isArray spec looks like the most under-specd thing I could find in latest ECMAScript ... it throws a true to anything without even thinking about it, and while I am sure it can't be changed now that it's doomed by default, at least please let's add a new Proxy trap behavior to avoid people patching Array.isArray all over the inevitably proxied world that both WASM and Worker offer, thank you!

That is what I mean by "special case"—you cannot replicate this with user objects :) I don't think the committee is going to pursue this behavior in the future either, but legacy behavior can't be changed.

it throws a true to anything without even thinking about it

It returns true for exactly two things: an array (whatever realm it comes from), and a proxy for an array. I'm not sure what surprising behavior can be caused by that, other than a possible leak-of-abstraction.

1 Like

the leak of abstraction is what any WASM or Worker would have but the fact Array.isArray(ref) an then ref.map(fail) is an hazard with current specs ... you provided an "evil" example to prove my point but my point is that I am proxying a reference and I want exclusive handling over that reference.

Every single (weird exception a part but not part of this thread) operation works out of the box but then Array.isArray(ref) is able, in a single check, to branch differently every single library that should not understand, reveal, or deal with, my proxied references there for a well defined purpose.

In case anyone is wondering, return new Proxy(Array.isArray(thing) ? thing : {thing}) is not an answer for the very same reason I am concerned in here: people currently need an Array.isArray monkey patch to avoid this Proxy shenanigan and nobody can ensure that Array.isArray call is not tainted already out there (or it won't need to be patched to satisfy results) ... screwing developers intent in the making.

Can we please think about a trap for this check which is AFAIK the only one that drills proxies beside apply and construct that need something different from a typeof "object" to work?

Thank You!

The topic is moving way too fast for me, but I think the gist of the surprise comes from the fact Array.isArray does a brand check (aka checks the presence / value of an internal slot).

It's the only static method to do so in the standard library. It is indeed a legacy decision, and since then the guideline has been to only do brand checking on this, not on method arguments (static or otherwise).

That said that behavior is consistent with proxied functions, in the sense that the typeof operator does a brand check as well for callable objects.

The motivation is to allow building faithful membranes, so I am a little confused by how you find this behavior surprising since it seems you're trying to use proxies for a similar use case.

I don't understand this. It is the job of your proxy handlers traps to also proxy the map property. The above only fails if your proxy fails to do its job.

People have building membrane libraries for over a decade, and none have had to patch Array.isArray.

There are quite a few large production systems that do just what you claim requires patching Array.isArray, and they don't have to do it. I'm not sure where the misunderstanding is, but I would read more into public membrane implementations to find out.

Well you can, but not as the target, which is special and captures/represents the "kind" and invariants of the proxy instance.

You can use a closure or a WeakMap to keep your data associated with the proxy instance and its actual target.

It could be an array from another realm, or it could be a subclass of Array that had its [[Prototype]] set to something that doesn't extend Array - or, it could be a Proxy to an array whose traps make it not instanceof Array.

Again, you can't generically wrap all JS values in a Proxy - you always have to branch based on what it is, or, restrict yourself to a subset of values, in order to imitate the thing properly (and in many cases, it's impossible unless you also patch builtins).

Array.isArray is special indeed, in that it and typeof are the only things that pierce through a Proxy.

can anyone show me a single code base where after an Array.isArray(ref) there is also a ref instance of Array brand check?

@mhofman I am not answering you as you are assuming I don't know what I am doing, what I am talking about, and why this issue was filed in the first place ... but you should stop assuming that from people raising issues in here, and the "nobody in 10 years had issues with it" narrative is extremely lame as I get paid to do stuff nobody did before, and quite successfully to date, so please try to be less dismissive and more open minded, thanks.

edit yes, nobody in the last 10 years did anything I am doing, and working, today, so thanks for asking, I don't want to make this thread a personal library goal on purpose, as I've mentioned in the first post, otherwise you'd be amused by what I'm doing and why this is a real-world issue out there.

I agree that isArray's behavior is weird. But I really don't think it warrants a new trap.

Proxies already have certain other restrictions on which behaviors they are allowed to customize - the typeof example is already mentioned, but there's also, for example, the [[IsExtensible]] trap, which is required to return the same value as that trap on the underlying object.

So there's always certain things that you need to know when establishing your Proxy - you can't blindly make a Proxy from an arbitrary target and then try to customize its behavior to act completely like something else after-the-fact. You really do need to figure out certain details of what you're going to be pretending to be when you set up the Proxy. "Whether you are going to be pretending to be an array" is only one example among several of things you need to know up front.

(You mentioned that new Proxy(Array.isArray(thing) ? thing : {thing}) wouldn't work for you, but I don't understand why - all you said is that isArray might be patched, but of course new Proxy might be patched; you generally can't be defensive against earlier-running code patching stuff.)

1 Like