Skip to content

Instantly share code, notes, and snippets.

@littledan
Created August 6, 2020 02:40
Show Gist options
  • Save littledan/ab73ff08f98f33088a0072ad202445b1 to your computer and use it in GitHub Desktop.
Save littledan/ab73ff08f98f33088a0072ad202445b1 to your computer and use it in GitHub Desktop.
`private.name`: References to private fields and methods

Private fields and methods can only be accessed from lexically inside the class where they are defined. In many cases, it is important for code more generally to be able to access a private field or method. It's possible to close over access to private fields and methods to grant some other code the capability to access them, but the code to do this is awkward and redundant.

The private.name syntax creates a reified reference to accessing a private field or method without the need for repetitive arrow functions. As an example of usage:

class C {
  #x;
  static x = private.name #x;
}
c = new C;
C.x.has(c);      // true
C.x.get(c);      // undefined
C.x.set(c, 1);
C.x.get(c);      // 1

C.x.has({});     // false
C.x.get({});     // TypeError
C.x.set({}, 1);  // TypeError

Details of the PrivateName class

PrivateName is a constructor, but it throws when called or constructed. It has the following methods:

  • get(obj) gets the private field or method or getter on the object, or throws a TypeError if missing
  • set(obj, value) sets the private field or calls a setter on the object, or throws a TypeError if missing (or on a method)
  • has(obj) returns true if the object has the private field or method, and false otherwise.

All methods throw a TypeError if passed an argument which is not an object.

The method names here are designed to look "backwards" but make sense directly for ordinary JavaScript programming, and correspond to the WeakMap analogy for private fields when thinking about semantics more deeply.

Why not include an .add() method?

Private fields and methods are always added to instances in exactly one place: in the constructor, either at the beginning (for a base class) or when super() returns (for a subclass). Adding a capability for private name references to add a private field or method to an instance in a different way weakens this guarantee, risking violating expectations for class users. The risk is especially serious given how the PrivateName can be shared outside of the class.

Frozen class and instances

Justin Ridgewell argued that, as private fields and methods have syntax that gives it integrity, then metaprogramming over them should also have integrity. To Justin, this would imply that the class's prototype would be frozen, to prevent code from modifying it. This constraint differs from TC39's usual analysis, which assumes that early-running code can be responsible for any freezing necessary; the logic here is that PrivateName represents an especially important case for integrity in practice, even when early-running code may be impractical. This is a point I don't have a strong opinion on; I could see the argument either way.

Bikeshedding the choice of keyword

This gist uses the name private.name for the keyword, following the keyword.metaproperty variable scheme of ES6, e.g., new.target. This syntax avoids syntax issues like the need to ban newlines after contextual keywords. It also evocatively describes what the construct does.

Referring to a private name and exposing it to someone else is a bit of a "big deal"/"powertool", moreso than reading or writing a private field, so the name does not seem excessively/inappropriately long to me.

Some alternatives considered and not selected:

  • ref #foo: ref is a bit of a cryptic name, and might lead to expectations that it could reference other things (like lexically scoped variables).
  • &#foo: This comes to mind for a lot of people, but it's just too much punctuation!
  • #foo: A bare private name would be a bit strange, since it would feel like you'd get something that represents the whole name. However, (1) this construct is less powerful than the private name (2) it creates a new instance each time it executes.

Future related work

  • #name in obj: terser, simpler syntax for (private.name #name).has(obj) which avoids allocating a new PrivateName
  • Decorators on private fields and methods: Decorated private fields and methods receive a PrivateName instance in place of the property key (early draft)

Appendix:Polyfill/desugaring

private.name #foo

would desugar into something like

makePrivateName(x => x.#foo, (x, v) => x.#foo = v)

which could be defined as follows:

class PrivateName {
  #get;
  #set;
  
  get(obj) {
    return this.#get(obj);
  }
  
  set(obj, val) {
    return this.#set(obj, val);
  }
  
  has(obj) {
    try {
      this.#get(obj);
      return true;
    } catch {
      return false;
    }
  }

  static #allow = false;  // Prevent calling the constructor
  static make(get, set) {
    PrivateName.#allow = true;
    const name = new PrivateName(get, set);
    PrivateName.#allow = false;
    return name;
  }
  constructor(get, set) {
    if (!PrivateName.#allow) throw new TypeError;
    this.#get = get;
    this.#set = set;
  }
}

const makePrivateName = PrivateName.make;
delete PrivateName.make;

Object.freeze(PrivateName);
Object.freeze(PrivateName.prototype);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment