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

Craig Buckler
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.

Frequently Asked Questions (FAQs) about JavaScript Private Class Fields

What are the benefits of using private class fields in JavaScript?

Private class fields in JavaScript provide a way to encapsulate or hide data within a class, which is a fundamental principle of object-oriented programming. This encapsulation ensures that the internal state of an object can only be changed in ways that are explicitly defined by the class. This leads to more robust and predictable code, as it prevents external code from inadvertently changing the object’s state in unexpected ways. Additionally, private fields can help to simplify the interface of a class, as they reduce the number of properties and methods that are exposed to the outside world.

How do I declare a private class field in JavaScript?

In JavaScript, private class fields are declared by prefixing the field name with a hash (#) symbol. For example, to declare a private field named ‘value’ within a class, you would write #value. This field would then be accessible only within methods of the class, and not from outside the class.

Can I access a private class field from outside the class?

No, private class fields in JavaScript are not accessible from outside the class. This is by design, as one of the main purposes of private fields is to hide internal data from the outside world. If you attempt to access a private field from outside the class, JavaScript will throw a syntax error.

Can I use private class fields in conjunction with public fields?

Yes, a JavaScript class can have both private and public fields. Public fields are declared in the same way as private fields, but without the hash (#) prefix. Public fields can be accessed and modified from both inside and outside the class, unlike private fields which can only be accessed from within the class.

How do private class fields differ from private methods in JavaScript?

Private class fields and private methods in JavaScript serve similar purposes, in that they both provide a way to hide internal details of a class from the outside world. However, they are used in different ways. Private fields are used to store data that is only accessible within the class, while private methods are functions that can only be called from within the class.

Can I use private class fields in all JavaScript environments?

Private class fields are a relatively new feature in JavaScript, and as such they are not supported in all environments. As of the time of writing, private fields are supported in the latest versions of most major browsers, including Chrome, Firefox, and Safari. However, they are not supported in Internet Explorer. If you need to write code that works in all browsers, you may need to use a transpiler like Babel to convert your code into a form that older browsers can understand.

How do I use a private class field within a method of the class?

To use a private class field within a method of the class, you simply refer to the field by its name, prefixed with the hash (#) symbol. For example, if you have a private field named ‘value’, you could access it within a method like this: this.#value.

Can I use private class fields in subclasses?

Yes, private class fields can be used in subclasses in JavaScript. However, each subclass has its own separate set of private fields, which are not shared with the superclass or with other subclasses. This means that if a subclass declares a private field with the same name as a private field in the superclass, the two fields are completely separate and do not interfere with each other.

Can I use private class fields in static methods?

No, private class fields cannot be used in static methods in JavaScript. This is because static methods are associated with the class itself, rather than with instances of the class, and private fields are only accessible within instances of the class.

Can I iterate over private class fields in JavaScript?

No, private class fields in JavaScript are not included in iterations over the properties of an object. This is by design, as one of the main purposes of private fields is to hide internal data from the outside world. If you need to iterate over the properties of an object, you should use public fields or methods instead.