In modern JavaScript (ES2022+), Private Fields are a major language feature that allows developers to truly encapsulate data within a class. Unlike the traditional convention of using an underscore (_variable) to hint that a property is internal, private fields are enforced by the JavaScript engine itself.

Private fields are defined using the hash (#) prefix. Once a property is marked with a hash, it cannot be accessed, modified, or even detected from outside the class’s curly braces.

1. Syntax and Core Implementation

To declare a private field, you must define it at the top level of the class body. You cannot create private fields on the fly inside a constructor without first declaring them.

JavaScript
class BankAccount {
  // 1. Declare the private field
  #balance = 0;

  constructor(owner, initialDeposit) {
    this.owner = owner;
    this.#balance = initialDeposit;
  }

  // 2. Private data is accessible inside class methods
  getAccountSummary() {
    return `${this.owner} has a balance of $${this.#balance}`;
  }
}

const myAccount = new BankAccount("Alice", 1000);

console.log(myAccount.getAccountSummary()); // "Alice has a balance of $1000"
// console.log(myAccount.#balance); // ❌ SyntaxError: Private field '#balance' must be declared in an enclosing class

2. Hard Encapsulation vs. Conventions

The primary difference between # and _ is how the engine handles them.

  • Hard Privacy: Private fields are not included in Object.keys(), for…in loops, or JSON.stringify(). They cannot be accessed even through “backdoor” methods like account[“#balance”].
  • The “In” Operator: You can check for the presence of a private field using the in operator, which helps prevent errors when interacting with other instances of the same class.
JavaScript
class Secure {
  #secret = "Top Secret";

  static check(obj) {
    return #secret in obj; // Returns true if obj is an instance of Secure
  }
}

3. Private Methods and Accessors

Privacy isn’t limited to data variables. You can also make methods and getters/setters private to hide internal logic and “helper” functions from the user.

JavaScript
class CoffeeMachine {
  #waterAmount = 0;

  // Private method: Internal logic hidden from the user
  #checkWater() {
    if (this.#waterAmount <= 0) throw new Error("Out of water!");
  }

  fill(amount) {
    this.#waterAmount += amount;
  }

  brew() {
    this.#checkWater(); // Accessing internal private method
    console.log("Brewing your coffee...");
    this.#waterAmount -= 10;
  }
}

const machine = new CoffeeMachine();
machine.fill(100);
machine.brew(); // Works
// machine.#checkWater(); // ❌ Error: Private method is not accessible

4. Inheritance and Private Fields

A key nuance of private fields is that they are not inherited by subclasses. Even though a child class extends a parent, it does not have access to the parent’s # fields. This ensures that a parent class can change its internal implementation without accidentally breaking subclasses that might have tried to rely on those internals.

JavaScript
class Parent {
  #internalId = 123;
}

class Child extends Parent {
  showId() {
    // console.log(this.#internalId); // ❌ Error: Property '#internalId' does not exist on type 'Child'
  }
}

5. Use Case: Protecting Internal State

Private fields are ideal for classes that manage complex states, such as a “State Store” or an “API Client,” where exposing internal counters or tokens could lead to bugs if modified by outside scripts.

JavaScript
class APIClient {
  #apiKey;
  #requestCount = 0;

  constructor(key) {
    this.#apiKey = key;
  }

  fetchData(endpoint) {
    this.#requestCount++;
    return fetch(`${endpoint}?key=${this.#apiKey}`);
  }

  get usage() {
    return this.#requestCount; // Expose the count, but not the key
  }
}

Categorized in:

Javascript,