Strong brand checking in JavaScript

I'd like to discuss a pattern of brand checking in JavaScript. Jordan Harband has been asking that each proposal have a way to detect this, but I would prefer that we find a general scheme, rather than have each proposal find a unique solution. I see two paths:

  • Type.isType: The idea here would be that we add more functions similar to Array.isArray, like Map.isMap, etc. (It's up for debate whether these are proxy-transparent or not. My view is that these should not be Proxy-transparent, but Mark Miller has expressed the opposite view.) It's important that Type.isType use internal slots, and not an overridable symbol, for reliability (which is the whole point of this proposal).
  • Restoring the previous Object.prototype.toString semantics. The original value Object.prototype.toString gives a strong way to check the type of built-ins. This was changed in ES6 to use Symbol.toStringTag, which removes the reliability that it previously had, and adds performance cost, while serving no use cases that I'm aware of. Just like TC39 has recently considered backing out Symbol.species semantics, we could consider backing out the Symbol.toStringTag semantics.

Either of these options may be controversial, but I think we would benefit significantly by making a general decision than several ad-hoc ones on a case-by-case basis to support brand checking.



Wasn't the idea to make polyfilling new types easier (without overriding Object.prototype.toString)? Of course, polyfilling clashes with the desire for reliability. I don't have a solution for that either.


1 Like

To clarify, committee consensus when Symbol.toStringTag was left in ES6 was that everything must have a way to brand-check, even if that was an accessor or method that threw an exception.

I am very much on board with a general proposal for this, so that individual proposals do not have to figure it out each time, and so the web can begin to use it on their own primitives - but the committee has not accepted such an attempt in the past.

While I can safely claim that removing Symbol.toStringTag is not web compatible, changing it (on builtins) from a string data property, to a brand-checking accessor that returns a string, would likely be a web-compatible change, and would satisfy brand-checking needs while serving as a general mechanism moving forward.

My main concern about brand checking APIs is that people will use them when they should be doing a duck type check instead.

1 Like

@ljharb Can you say more about the nature of the web compatibility issue? Is this a matter of Symbol.toStringTag existing, or about how it's used by Object.prototype.toString?

@devsnek Should we ensure that the brand check APIs remain unergonomic?

What I mean is, I believe many people are likely implementing Symbol.toStringTag, including in DOM polyfills, and now relying on Object.prototype.toString output will contain it. Additionally, both core-js and es-shims check for missing Symbol.toStringTag output via calling Object.prototype.toString. There are use cases for it - the only real problem is that they're all string data properties instead of brand-checking accessors.

@devsnek there are a myriad of kinds of checks they can already do ergonomically, and often only one of them is appropriate for the situation. A brand check isn't any less likely to be the appropriate choice than any of the others.

I'm not really in favor of adding explicit brand checking methods, but if we're going to do it, I do have an opinion on whether these are proxy-transparent or not: they should not be.

This is because none of the existing methods which perform brand checks as part of their operation are proxy-transparent: Proxy(new Map, {}), "key") will throw, for example. So it would be very strange for Map.isMap(new Proxy(new Map, {})) to return true.


That's an excellent point and good choice of a guideline @bakkot! I'd specify

A brand checking method should be proxy-transparent IFF the brand's instance methods are proxy-transparent.

Array.isArray(new Proxy([], {})) is true as new Proxy([], {}).slice() works fine, whereas Map.isMap(new Prox(new Map, {})) should be false because new Proxy(new Map, {}).keys() throws.

Hopefully nobody is anxious to repeat the bizarre behavior of Array.isArray punching through proxies, given that the rest of the language already has a different behavior.

1 Like

@ljharb If the uses are all through a few central polyfills, we might be able to look into the use cases and see if it would be compatible to change how it's used (while leaving the symbol in place). That's the plan with Symbol.species. Do you think this approach might work for Symbol.toStringTag?

@bakkot I agree that any brand-checking methods should not be proxy-transparent. Could you explain why you're not really in favor of adding explicit brand-checking methods?

1 Like

If you mean to change how a Object.prototype.toString works, or change that Get of Symbol.toStringTag produced a string, then i don’t; old versions of polyfills are widely deployed and are unlikely to be updated in sufficient time to change the landscape. Changing them to brand-checking getters, however, should be entirely compatible.

A general dislike of adding new reflection mechanisms in the absence of a strong reason to do so, basically. I'd be OK with it given sufficient reason, but if that's been discussed I missed it.


Object.kind(thing) : string

that returns "Object", "Array", etc.

In browsers Object.kind(document.body) shall return "Element", "Comment", "Text", etc. name of native class/entity that is a "driver" of objects of that kind.

The kind is not a class name but really is an entity kind marker.

Each JS implementation has "drivers" already and internal implementations of Object.kind() that are actively used in debugging support for example.

I think it would be best if, whatever mechanism we come up with, it is reasonable to implement a similar API for user-defined types. That's one reason I like the API MyClass.isMyClass(value).


A benefit of boolean based MyClass.isMyClass(value) is that these can be defined in userland TypeScript.

// while proposal is still in user-land...

class NewProposal {
  static isNewProposal(value: unknown): value is NewProposal {
    // implementation left for reader

  method() {}

if (NewProposal.isNewProposal(someValue)) {
   // in here TS gives `someValue` the type `NewProposal`

Whereas string checks, Brands.get(value) === 'well known string', require native TypeScript support.

if (typeof randomValue === 'string') {
  // TS natively understands the knowledge gained by the string equality; // safe

if (Brands.get(someValue) === 'Well Known Brand') {
  // no current way in userland to 'teach' TS how to use the above
  // to narrow the type of `someValue`
  someValue.method(); // TS error
1 Like