JavaScript’s New Private Class Fields, and How to Use Them

Craig Buckler
Share

ES6 introduced classes to JavaScript, but they can be too simplistic for complex applications. Class fields (also referred to as class properties) aim to deliver simpler constructors with private and static members. The proposal is currently a TC39 stage 3: candidate and is likely to be added to ES2019 (ES10). Private fields are currently supported in Node.js 12, Chrome 74, and Babel.

A quick recap of ES6 classes is useful before we look at how class fields are implemented.

This article was updated in 2020. For more in-depth JavaScript knowledge, read our book, JavaScript: Novice to Ninja, 2nd Edition.

ES6 Class Basics

JavaScript’s object-oriented inheritance model can confuse developers coming from languages such as C++, C#, Java, and PHP. For this reason, ES6 introduced classes. They are primarily syntactical sugar but offer more familiar object-oriented programming concepts.

A class is an object template which defines how objects of that type behave. The following Animal class defines generic animals (classes are normally denoted with an initial capital to distinguish them from objects and other types):

class Animal {

  constructor(name = 'anonymous', legs = 4, noise = 'nothing') {

    this.type = 'animal';
    this.name = name;
    this.legs = legs;
    this.noise = noise;

  }

  speak() {
    console.log(`${this.name} says "${this.noise}"`);
  }

  walk() {
    console.log(`${this.name} walks on ${this.legs} legs`);
  }

}

Class declarations always execute in strict mode. There’s no need to add 'use strict'.

The constructor method is run when an object of the Animal type is created. It typically sets initial properties and handles other initializations. speak() and walk() are instance methods which add further functionality.

An object can now be created from this class with the new keyword:

let rex = new Animal('Rex', 4, 'woof');
rex.speak();          // Rex says "woof"
rex.noise = 'growl';
rex.speak();          // Rex says "growl"

Getters and Setters

Setters are special methods used to define values only. Similarly, Getters are special methods used to return a value only. For example:

class Animal {

  constructor(name = 'anonymous', legs = 4, noise = 'nothing') {

    this.type = 'animal';
    this.name = name;
    this.legs = legs;
    this.noise = noise;

  }

  speak() {
    console.log(`${this.name} says "${this.noise}"`);
  }

  walk() {
    console.log(`${this.name} walks on ${this.legs} legs`);
  }

  // setter
  set eats(food) {
    this.food = food;
  }

  // getter
  get dinner() {
    return `${this.name} eats ${this.food || 'nothing'} for dinner.`;
  }

}

let rex = new Animal('Rex', 4, 'woof');
rex.eats = 'anything';
console.log( rex.dinner );  // Rex eats anything for dinner.

Child or Sub-classes

It’s often practical to use one class as the base for another. A Human class could inherit all the properties and methods from the Animal class using the extends keyword. Properties and methods can be added, removed, or changed as necessary so human object creation becomes easier and more readable:

class Human extends Animal {

  constructor(name) {

    // call the Animal constructor
    super(name, 2, 'nothing of interest');
    this.type = 'human';

  }

  // override Animal.speak
  speak(to) {

    super.speak();
    if (to) console.log(`to ${to}`);

  }

}

super refers to the parent class, so it’s usually the first call made in the constructor. In this example, the Human speak() method overrides that defined in Animal.

Object instances of Human can now be created:

let don = new Human('Don');
don.speak('anyone');        // Don says "nothing of interest" to anyone

don.eats = 'burgers';
console.log( don.dinner );  // Don eats burgers for dinner.

Static Methods and Properties

Defining a method with the static keyword allows it to be called on a class without creating an object instance. Consider the Math.PI constant: there’s no need to create a Math object before accessing the PI property.

ES6 doesn’t support static properties in the same way as other languages, but it is possible to add properties to the class definition itself. For example, the Human class can be adapted to retain a count of how many human objects have been created:

class Human extends Animal {

  constructor(name) {

    // call the Animal constructor
    super(name, 2, 'nothing of interest');
    this.type = 'human';

    // update count of Human objects
    Human.count++;

  }

  // override Animal.speak
  speak(to) {

    super.speak();
    if (to) console.log(`to ${to}`);

  }

  // return number of human objects
  static get COUNT() {
    return Human.count;
  }

}

// static property of the class itself - not its objects
Human.count = 0;

The class’s static COUNT getter returns the number of humans accordingly:

console.log(`Humans defined: ${Human.COUNT}`); // Humans defined: 0

let don = new Human('Don');

console.log(`Humans defined: ${Human.COUNT}`); // Humans defined: 1

let kim = new Human('Kim');

console.log(`Humans defined: ${Human.COUNT}`); // Humans defined: 2

ES2019 Class Fields (NEW)

The new class fields implementation allows public properties to initialized at the top of a class outside any constructor:

class MyClass {

  a = 1;
  b = 2;
  c = 3;

}

This is equivalent to:

class MyClass {

  constructor() {
    this.a = 1;
    this.b = 2;
    this.c = 3;
  }

}

If you still require a constructor, initializers will be executed before it runs.

Static Class Fields

In the example above, static properties were inelegantly added to the class definition object after it had been defined. This isn’t necessary with class fields:

class MyClass {

  x = 1;
  y = 2;
  static z = 3;

}

console.log( MyClass.z ); // 3

This is equivalent to:

class MyClass {

  constructor() {
    this.x = 1;
    this.y = 2;
  }

}

MyClass.z = 3;

console.log( MyClass.z ); // 3

Private Class Fields

All properties in ES6 classes are public by default and can be examined or modified outside the class. In the Animal examples above, there’s nothing to prevent the food property being changed without calling the eats setter:

class Animal {

  constructor(name = 'anonymous', legs = 4, noise = 'nothing') {

    this.type = 'animal';
    this.name = name;
    this.legs = legs;
    this.noise = noise;

  }

  set eats(food) {
    this.food = food;
  }

  get dinner() {
    return `${this.name} eats ${this.food || 'nothing'} for dinner.`;
  }

}

let rex = new Animal('Rex', 4, 'woof');
rex.eats = 'anything';      // standard setter
rex.food = 'tofu';          // bypass the eats setter altogether
console.log( rex.dinner );  // Rex eats tofu for dinner.

Other languages often permit private properties to be declared. That’s not possible in ES6, so developers often work around it using the underscore convention (_propertyName), closures, symbols, or WeakMaps. An underscore provides a hint to the developer, but there’s nothing to prevent them accessing that property.

In ES2019, private class fields are defined using a hash # prefix:

class MyClass {

  a = 1;          // .a is public
  #b = 2;         // .#b is private
  static #c = 3;  // .#c is private and static

  incB() {
    this.#b++;
  }

}

let m = new MyClass();

m.incB(); // runs OK
m.#b = 0; // error - private property cannot be modified outside class

Note that there’s no way to define private methods, getters, or setters. A TC39 stage 3: draft proposal suggests using a hash # prefix on names and it has been implemented in Babel. For example:

class MyClass {

  // private property
  #x = 0;

  // private method (can only be called within the class)
  #incX() {
    this.#x++;
  }

  // private setter (can only be used within the class)
  set #setX(x) {
    this.#x = x;
  }

  // private getter (can only be used within the class)
  get #getX() {
    return this.$x;
  }

}

Immediate Benefit: Cleaner React Code!

React components often have methods tied to DOM events. To ensure this resolves to the component, it’s necessary to bind every method accordingly. For example:

class App extends Component {

  constructor() {

    super();

    this.state = { count: 0 };

    // bind all methods
    this.incCount = this.incCount.bind(this);
  }

  incCount() {
    this.setState(ps => { count: ps.count + 1 })
  }

  render() {

    return (
      <div>
        <p>{ this.state.count }</p>
        <button onClick={this.incCount}>add one</button>
      </div>
    );

  }
}

When incCount is defined as an ES2019 class field, it can be assigned as a function using the ES6 => fat arrow, which is automatically bound to the defining object. It’s no longer necessary to add bind declarations:

class App extends Component {

  state = { count: 0 };

  incCount = () => {
    this.setState(ps => { count: ps.count + 1 })
  };

  render() {

    return (
      <div>
        <p>{ this.state.count }</p>
        <button onClick={this.incCount}>add one</button>
      </div>
    );

  }
}

Class Fields: an Improvement?

ES6 class definitions were simplistic. ES2019 class fields require less code, aid readability, and enable some interesting object-oriented programming possibilities.

Using # to denote privacy has received some criticism, primarily because it’s ugly and feels like a hack. Most languages implement a private keyword, so attempting to use that member outside the class will be rejected by the compiler.

JavaScript is interpreted. Consider the following code:

class MyClass {
  private secret = 123;
}

const myObject = new MyClass();
myObject.secret = 'one-two-three';

This would have thrown a runtime error on the last line, but that’s a severe consequence for simply attempting to set a property. JavaScript is purposely forgiving and ES5 permitted property modification on any object.

Although clunky, the # notation is invalid outside a class definition. Attempting to access myObject.#secret can throw a syntax error.

The debate will continue but, like them or not, class fields have been adopted in several JavaScript engines. They’re here to stay.