JS Proxy and private properties

The default Proxy implementation doesn't work well with private properties, but we can fix this! I also explore other approaches to private data in JS.

What is Proxy?

The JS Proxy class allows you to add functions that hook into internal JS features such as reading and writing object properties. Vue uses it to implement its reactivity system by exposing Proxy wrapped objects to framework users instead of the real object.

Proxy has been supported in all browsers since 2016.

const object = { x: 4, y: 10 };

const proxy = new Proxy(object, {
  get(target, property, thisValue) {
    if (property === "x") {
      return -target.x;
    }
    // The `Reflect` class contains the default
    // implementations of the `Proxy` hooks,
    // for your convenience
    return Reflect.get(target, property, thisValue);
  },
});

console.log(object.y); //=> 10
console.log(object.x); //=> 4

console.log(proxy.y); //=> 10
console.log(proxy.x); //=> -4

You can implement many powerful patterns with Proxy, such as objects that throw errors when you try to access missing keys, or listen for modifications to the properties of an object without using setters.

What are private properties?

Private properties have been supported in all browsers since 2021.

Private properties are object properties that can only be accessed from methods defined inside the class declaration of an object’s constructor. These are usually combined with setters and getters to define custom behavior when reading/writing object properties.

class Thing {
  #x;

  constructor(value) {
    this.#x = value;
  }

  set x(value) {
    console.log("SET x", value);
    this.#x = value;
  }

  get x() {
    console.log("GET x");
    return this.#x;
  }
}

const thing = new Thing("secret");
thing.x;
//=> "GET x"
console.log(thing.x);
//=> "GET x"
//=> "secret"
console.log(thing.#x);
// SyntaxError: can't access private property
// outside of class definition

thing.x = "new_secret";
//=> "SET x new_secret"
console.log(thing.x);
//=> "GET x"
//=> "new_secret"

What happens when you combine them?

You might expect that if you make a Proxy but don’t add any hook function, that the new proxy would behave more or less identicaly to the original object.

This is mostly true, but unfortunately methods that reference private properties will crash by default.

class CoolValue {
  #value;

  constructor(value) {
    this.#value = value;
  }

  get value() {
    return this.#value;
  }

  logValue() {
    console.log(this.#value);
  }
}

const v = new CoolValue("hello");
console.log(v.value);
//=> "hello"

const p = new Proxy(v, {});
console.log(p.value);
// TypeError: can't access private field or method:
// object is not the right class
p.logValue();
// TypeError: can't access private field or method:
// object is not the right class

We can fix this problem by defining a different get hook for the Proxy.

const p = new Proxy(v, {
  get(target, prop, thisValue) {
    // Ignore `thisValue`, which is the proxy itself.
    // `target` is the original object known here as `v`.
    // This could also be written as `target[prop]`.
    const value = Reflect.get(target, prop, target);
    // If the value is a function, we need to bind the function
    // to use the correct `this` value of `target`,
    // the underlying object. Otherwise `this` will be
    // set to `thisValue`, which is the proxy.
    // The proxy doesn't have access to the
    // private properties of the class.
    if (typeof value === "function") {
      return value.bind(target);
    }
    return value;
  },
});
console.log(p.value);
p.logValue();

What about WeakMap?

WeakMap can be used to simulate private data, but the behavior will be even worse. Rather than throwing errors about not having access to private data, the underlying map will simply return undefined with no explanation.

const $value = new WeakMap();
class CoolValue {
  constructor(value) {
    $value.set(this, value);
  }

  get value() {
    return $value.get(this);
  }

  logValue() {
    console.log($value.get(this));
  }
}

const v = new CoolValue("hello");
console.log(v.value);
//=> "hello"

const p = new Proxy(v, {});
console.log(p.value);
//=> undefined
p.logValue();
//=> undefined

What about Symbol keys?

Using secret Symbol values as the keys for “private” values seems to work really well. It’s a bit annoying compared to private properties, and it can circumvented if you try really hard, but these properties are invisible to most JS methods (e.g. Object.keys or JSON.stringify).

// The Symbol name is optional, but it's
// good practice to provide one for
// debugging purposes. Otherwise every Symbol key
// will just look like Symbol() in the
// Developer Tools when inspecting an object.
const $value = Symbol("CoolValue.value");

class CoolValue {
  constructor(value) {
    this[$value] = value;
  }

  get value() {
    return this[$value];
  }

  logValue() {
    console.log(this[$value]);
  }
}

const v = new CoolValue("hello");
console.log(v.value);
//=> "hello"

const p = new Proxy(v, {});
console.log(p.value);
//=> "hello"
p.logValue();
//=> "hello"

In fact, JS already uses Symbol for pseudo-private properties like Symbol.toStringTag and Symbol.iterator. It seems like this feature was added primarily to allow multiple types of APIs to exist on an object, without requiring names to be unique (all Symbol values are not equal to each other—like objects—even if they have the same display name).

Inspired by Lea Verou

I was originally inspired by Lea Verou’s post JS private class fields considered harmful.

I wanted to find if there was a workaround for the problem she described. I’m not a seasoned Vue developer like she is, so I’ll assume that I’m still missing some corner case of this solution that makes it not work at least for how Vue wants to use Proxy. But I’m glad to see you can fix it in the simple case, at least.

Combinations of features can be surprising

It’s never a pleasant feeling when two features of a language don’t combine well like Proxy and private properties. I can’t help but feel that private properties should’ve just been syntax sugar over a Symbol made behind the scenes anyway. Maybe I’m just a minimalist, but I feel like building on top of existing features is good. But I also remember programming in JS before ES5 came out, and frankly it was still a pretty good language even back then (hot take).

Yes, you can use Object.getOwnPropertySymbols to enumerate the symbols for an object, but this is nearly impossible to do on accident. If I had the time and energy, maybe I could browse the GitHub discussions for why the private property proposal went with true privacy instead of just being great syntax sugar over nearly-private fields using Sybmol keys.

Going forward with private properties

All of this mess around Proxy has me feeling a bit awkward about whether or not I should keep using private properties in my code in the future. I’ve written already about developing with web components wherein I use private properties extensively. I have no need for Proxy in that code, and the workaround I mentioned earlier could still be employed if I did.

I won’t deny that there’s a certain elegance to the idea behind hidden Symbol properties on objects, but the syntactic awkwardness will probably prevent me from using them in most code I work on.