How Private Class Fields Work in JavaScript

JavaScript private class fields, identified by the # prefix, provide built-in encapsulation by restricting property and method access strictly to the class body in which they are declared. This article explains the mechanics of the hash prefix syntax, demonstrates how it enforces true hard privacy at runtime, and covers its application across instance fields, static variables, methods, and accessors.

The Syntax for Private Fields

To define a private field, prefix its name with a # symbol within the class definition. You must declare private fields at the top level of the class body before using them.

class BankAccount {
  // Declare the private field
  #balance;

  constructor(initialBalance) {
    this.#balance = initialBalance;
  }

  deposit(amount) {
    this.#balance += amount;
  }

  getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount(100);
account.deposit(50);
console.log(account.getBalance()); // 150

// Attempting external access throws a SyntaxError
// console.log(account.#balance); 

How Hard Privacy Works

Unlike traditional naming conventions (like _variable) or TypeScript’s private keyword, JavaScript’s hash syntax enforces true runtime privacy:

  1. No External Access: Attempting to read or write a # field outside its defining class results in a syntax error at compile time or a runtime error.
  2. Invisible to Reflection: Private fields do not appear in Object.keys(), Object.getOwnPropertyNames(), or Reflect.ownKeys().
  3. No Bracket Notation: Private fields cannot be accessed dynamically using square brackets (e.g., this['#balance'] will look for a literal string property named "#balance" rather than the private field).
  4. No Inheritance Access: Subclasses cannot access private fields of a parent class. They belong exclusively to the declaring class scope.

Private Methods, Getters, and Setters

The # prefix extends beyond standard instance variables to include private methods, getters, and setters.

class User {
  #password;

  constructor(username, password) {
    this.username = username;
    this.#password = password;
  }

  // Private method
  #validatePassword(input) {
    return input === this.#password;
  }

  // Private getter
  get #maskedPassword() {
    return '****';
  }

  verify(input) {
    return this.#validatePassword(input);
  }
}

Private Static Fields

You can combine the static keyword with the # prefix to create private class-level properties and methods accessible only by the class itself.

class Counter {
  static #instanceCount = 0;

  constructor() {
    Counter.#incrementCount();
  }

  static #incrementCount() {
    Counter.#instanceCount++;
  }

  static get totalInstances() {
    return Counter.#instanceCount;
  }
}

new Counter();
new Counter();
console.log(Counter.totalInstances); // 2

Checking for Private Fields Using the in Operator

JavaScript allows you to check whether an object possesses a specific private field using the in operator inside the class scope. This provides a safe way to test an object’s brand without triggering errors.

class Person {
  #name;

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

  static isPerson(obj) {
    return #name in obj;
  }
}

const person = new Person('Alice');
const other = { name: 'Bob' };

console.log(Person.isPerson(person)); // true
console.log(Person.isPerson(other));  // false