Public and private class fields

Published · Tagged with ECMAScript ES2022

Several proposals expand the existing JavaScript class syntax with new functionality. This article explains the new public class fields syntax in V8 v7.2 and Chrome 72, as well as the upcoming private class fields syntax.

Here’s a code example that creates an instance of a class named IncreasingCounter:

const counter = new IncreasingCounter();
counter.value;
// logs 'Getting the current value!'
// → 0
counter.increment();
counter.value;
// logs 'Getting the current value!'
// → 1

Note that accessing the value executes some code (i.e., it logs a message) before returning the result. Now ask yourself, how would you implement this class in JavaScript? 🤔

ES2015 class syntax #

Here’s how IncreasingCounter could be implemented using ES2015 class syntax:

class IncreasingCounter {
constructor() {
this._count = 0;
}
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}

The class installs the value getter and an increment method on the prototype. More interestingly, the class has a constructor that creates an instance property _count and sets its default value to 0. We currently tend to use the underscore prefix to denote that _count should not be used directly by consumers of the class, but that’s just a convention; it’s not really a “private” property with special semantics enforced by the language.

const counter = new IncreasingCounter();
counter.value;
// logs 'Getting the current value!'
// → 0

// Nothing stops people from reading or messing with the
// `_count` instance property. 😢
counter._count;
// → 0
counter._count = 42;
counter.value;
// logs 'Getting the current value!'
// → 42

Public class fields #

The new public class fields syntax allows us to simplify the class definition:

class IncreasingCounter {
_count = 0;
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}

The _count property is now nicely declared at the top of the class. We no longer need a constructor just to define some fields. Neat!

However, the _count field is still a public property. In this particular example, we want to prevent people from accessing the property directly.

Private class fields #

That’s where private class fields come in. The new private fields syntax is similar to public fields, except you mark the field as being private by using #. You can think of the # as being part of the field name:

class IncreasingCounter {
#count = 0;
get value() {
console.log('Getting the current value!');
return this.#count;
}
increment() {
this.#count++;
}
}

Private fields are not accessible outside of the class body:

const counter = new IncreasingCounter();
counter.#count;
// → SyntaxError
counter.#count = 42;
// → SyntaxError

Public and private static properties #

Class fields syntax can be used to create public and private static properties and methods as well:

class FakeMath {
// `PI` is a static public property.
static PI = 22 / 7; // Close enough.

// `#totallyRandomNumber` is a static private property.
static #totallyRandomNumber = 4;

// `#computeRandomNumber` is a static private method.
static #computeRandomNumber() {
return FakeMath.#totallyRandomNumber;
}

// `random` is a static public method (ES2015 syntax)
// that consumes `#computeRandomNumber`.
static random() {
console.log('I heard you like random numbers…');
return FakeMath.#computeRandomNumber();
}
}

FakeMath.PI;
// → 3.142857142857143
FakeMath.random();
// logs 'I heard you like random numbers…'
// → 4
FakeMath.#totallyRandomNumber;
// → SyntaxError
FakeMath.#computeRandomNumber();
// → SyntaxError

Simpler subclassing #

The benefits of the class fields syntax become even clearer when dealing with subclasses that introduce additional fields. Imagine the following base class Animal:

class Animal {
constructor(name) {
this.name = name;
}
}

To create a Cat subclass that introduces an additional instance property, you’d previously have to call super() to run the constructor of the Animal base class before creating the property:

class Cat extends Animal {
constructor(name) {
super(name);
this.likesBaths = false;
}
meow() {
console.log('Meow!');
}
}

That’s a lot of boilerplate just to indicate that cats don’t enjoy taking baths. Luckily, the class fields syntax removes the need for the whole constructor, including the awkward super() call:

class Cat extends Animal {
likesBaths = false;
meow() {
console.log('Meow!');
}
}

Feature support #

Support for public class fields #

Support for private class fields #

Support for private methods and accessors #