Private brand checks a.k.a. #foo in obj

Published · Tagged with ECMAScript

The in operator can be used for testing whether the given object (or any object in its prototype chain) has the given property:

const o1 = {'foo': 0};
console.log('foo' in o1); // true
const o2 = {};
console.log('foo' in o2); // false
const o3 = Object.create(o1);
console.log('foo' in o3); // true

The private brand checks feature extends the in operator to support private class fields:

class A {
static test(obj) {
console.log(#foo in obj);
}
#foo = 0;
}

A.test(new A()); // true
A.test({}); // false

class B {
#foo = 0;
}

A.test(new B()); // false; it's not the same #foo

Since private names are only available inside the class which defines them, the test must also occur inside the class, for example in a method like static test above.

Subclass instances receive private fields from the parent class as own-properties:

class SubA extends A {};
A.test(new SubA()); // true

But objects created with with Object.create (or that have the prototype set later via the __proto__ setter or Object.setPrototypeOf) don't receive the private fields as own-properties. Because private field lookup only works on own-properties, the in operator does not find these inherited fields:

const a = new A();
const o = Object.create(a);
A.test(o); // false, private field is inherited and not owned
A.test(o.__proto__); // true

const o2 = {};
Object.setPrototypeOf(o2, a);
A.test(o2); // false, private field is inherited and not owned
A.test(o2.__proto__); // true

Accessing a non-existing private field throws an error - unlike for normal properties, where accessing a non-existent property returns undefined but doesn't throw. Before the private brand checks, the developers have been forced to use a try-catch for implementing fall-back behavior for cases where an object doesn't have the needed private field:

class D {
use(obj) {
try {
obj.#foo;
} catch {
// Fallback for the case obj didn't have #foo
}
}
#foo = 0;
}

Now the existence of the private field can be tested using a private brand check:

class E {
use(obj) {
if (#foo in obj) {
obj.#foo;
} else {
// Fallback for the case obj didn't have #foo
}
}
#foo = 0;
}

But beware - the existence of one private field does not guarantee that the object has all the private fields declared in a class! The following example shows a half-constructed object which has only one of the two private fields declared in its class:

let halfConstructed;
class F {
m() {
console.log(#x in this); // true
console.log(#y in this); // false
}
#x = 0;
#y = (() => {
halfConstructed = this;
throw 'error';
})();
}

try {
new F();
} catch {}

halfConstructed.m();

Private brand check support #