ECMAScript proposal: Ergonomic brand checks for private fields

[2021-06-19] dev, javascript, es proposal
(Ad, please don’t block)

In this blog post, we examine the ECMAScript proposal “Ergonomic brand checks for private fields” (by Jordan Harband). It proposes a compact way for checking if an object has a given private field.

Background on private fields  

For this blog post, it helps if you are familiar with private fields (but you’ll probably be fine if not). If you want to read up on this topic: I have written four blog posts about public fields and private members in classes.

Quick recap: Each class has its own private fields  

Let’s first review one important characteristic of private fields: They are unique per class. They are not even shared with subclasses.

Two different classes with private fields that have the same name  

The following example demonstrates what happens if two different classes both have a private field called .#name:

class Class1 {
  #name;
  constructor(name) {
    this.#name = name;
  }
  static getName(inst) {
    return inst.#name;
  }
}
class Class2 {
  #name;
  constructor(name) {
    this.#name = name;
  }
}

const inst1 = new Class1('inst1');
const inst2 = new Class2('inst2');

assert.equal(Class1.getName(inst1), 'inst1'); // (A)

assert.throws(
  () => Class1.getName(inst2), // (B)
  {
    name: 'TypeError',
    message: 'Cannot read private member #name from an object'
      + ' whose class did not declare it',
  });

Class1.getName() can access .#name in instances of Class1 (line A), but it can’t access that field in instances of Class2 (line B).

Private fields in classes created via factories  

Private fields being unique per class is even more interesting when dealing with class factories such as the following function createClass():

function createClass() {
  return class {
    #name;
    constructor(name) {
      this.#name = name;
    }
    static getName(inst) {
      return inst.#name;
    }
  };
}

const Class1 = createClass();
const Class2 = createClass();

If we use createClass() to create two classes Class1 and Class2, we see the same behavior as in the previous example:

assert.equal(Class1.getName(new Class1('inst1')), 'inst1');
assert.throws(
  () => Class1.getName(new Class2('inst2')),
  {
    name: 'TypeError',
    message: 'Cannot read private member #name from an object'
      + ' whose class did not declare it',
  });

Checking if a private field exists  

So how do we check if an object has a given private field?

Checking via instanceof doesn’t always work  

We may think we can use instanceof, but, alas, we can’t, as the next example demonstrates.

class MyClass1 {
  #name;
  static hasNameViaInstanceof(obj) {
    return obj instanceof MyClass1;
  }
}

The thinking is: All objects created via new MyClass1() have the private field .#name. Therefore, if, for an arbitrary object obj, the expression obj instanceof MyClass1 is true, we should always be able to access .#name.

This is indeed correct for all objects created via new MyClass1():

const realInstance = new MyClass1();
assert.equal(
  MyClass1.hasNameViaInstanceof(realInstance), true);

However, we can also create objects that are instances of MyClass1, but not created via new MyClass1():

const fakeInstance = Object.create(MyClass1.prototype);
assert.equal(
  MyClass1.hasNameViaInstanceof(fakeInstance), true); // (A)

Only the constructor of MyClass1 adds the private fields. Therefore, fakeInstance does not have the private field .#name and the result true in line A is wrong.

A safe way to check  

The following code contains a safe way to check if a private field exists:

class MyClass2 {
  #name;
  static hasNameViaAccess(obj) {
    try {
      obj.#name; // (A)
      return true; // (B)
    } catch (err) {
      if (err instanceof TypeError) {
        return false; // (C)
      }
      throw err;
    }
  }
}

Given an arbitrary object obj, we try to read the private field .#name (line A). If that works, we return true (line B). If it doesn’t, we return false (line C).

Note that we can’t do a generic check (via a parameter) for private fields because we only have fixed access.

Let’s see the results for a real instance and a fake instance:

const realInstance = new MyClass2();
assert.equal(
  MyClass2.hasNameViaAccess(realInstance), true);

const fakeInstance = Object.create(MyClass2.prototype);
assert.equal(
  MyClass2.hasNameViaAccess(fakeInstance), false);

Alas, this solution is verbose. That’s where the proposal comes in.

The proposal: checking via the in operator  

With the proposal, checking if an object has a given private field is much more convenient (line A):

class MyClass3 {
  #name;
  static hasNameViaIn(obj) {
    return #name in obj; // (A)
  }
}

const realInstance = new MyClass3();
assert.equal(
  MyClass3.hasNameViaIn(realInstance), true);

const fakeInstance = Object.create(MyClass3.prototype);
assert.equal(
  MyClass3.hasNameViaIn(fakeInstance), false);

More information on object-oriented programming in JavaScript  

In the book “JavaScript for impatient programmers” there are two chapters on object-oriented programming: