Door 21 | JS Adventskalender
Skip to content

Door 21

Published: at 07:00 AMSuggest Changes

Private Class Fields – Data Encapsulation in Classes

With the introduction of Private Class Fields in JavaScript, object orientation is taken to a new level. Previously, developers had to achieve data encapsulation with conventions (such as underscores before variable names) or closures, since all properties of objects were public by default. Private Class Fields provide a language-native solution to truly keep properties private.

What are Private Class Fields?

Private Class Fields are a syntactic extension in JavaScript that allows fields of a class to be declared as truly private. These fields are not visible from outside and can only be viewed or changed within class methods or accesses.

Characteristics of Private Class Fields:

Syntax Example

class Person {
  #name; // private field

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

  greet() {
    console.log(`Hello, my name is ${this.#name}.`);
  }
}

const p = new Person('Anna');
p.greet(); // "Hello, my name is Anna."
console.log(p.#name); // SyntaxError: Private field '#name' must be declared in an enclosing class

In this example, #name is a private field of Person. This means we cannot directly access p.#name from outside the class.

Why Use Private Fields?

1. True Encapsulation:

Before private fields, all properties were public. The only way to achieve privacy was through conventions or closures:

// Old way with convention
class OldPerson {
  constructor(name) {
    this._name = name; // Convention: _ means private
  }
}

// But still accessible!
const op = new OldPerson('Max');
console.log(op._name); // Works, but shouldn't be used

2. Better API Design:

class BankAccount {
  #balance = 0;

  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
      return true;
    }
    return false;
  }

  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
      return true;
    }
    return false;
  }

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

const account = new BankAccount();
account.deposit(100);
console.log(account.getBalance()); // 100
// account.#balance = 1000000; // SyntaxError

3. Private Methods:

Not just fields, but also methods can be private:

class DataProcessor {
  #validateData(data) {
    return data && data.length > 0;
  }

  #transformData(data) {
    return data.map(item => item.toUpperCase());
  }

  process(data) {
    if (!this.#validateData(data)) {
      throw new Error('Invalid data');
    }
    return this.#transformData(data);
  }
}

const processor = new DataProcessor();
console.log(processor.process(['hello', 'world']));
// ['HELLO', 'WORLD']

// processor.#validateData(['test']); // SyntaxError

4. Private Static Fields:

class Counter {
  static #count = 0;

  static increment() {
    Counter.#count++;
  }

  static getCount() {
    return Counter.#count;
  }
}

Counter.increment();
Counter.increment();
console.log(Counter.getCount()); // 2
// console.log(Counter.#count); // SyntaxError

Practical Use Cases

1. Singleton Pattern:

class Database {
  static #instance = null;
  #connected = false;

  constructor() {
    if (Database.#instance) {
      return Database.#instance;
    }
    Database.#instance = this;
  }

  connect() {
    this.#connected = true;
    console.log('Connected to database');
  }

  isConnected() {
    return this.#connected;
  }
}

const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true

2. State Management:

class StateMachine {
  #state = 'idle';
  #validTransitions = {
    idle: ['loading'],
    loading: ['success', 'error'],
    success: ['idle'],
    error: ['idle']
  };

  transition(newState) {
    if (this.#isValidTransition(newState)) {
      this.#state = newState;
      return true;
    }
    return false;
  }

  #isValidTransition(newState) {
    return this.#validTransitions[this.#state]?.includes(newState) ?? false;
  }

  getState() {
    return this.#state;
  }
}

const sm = new StateMachine();
console.log(sm.getState()); // 'idle'
sm.transition('loading'); // true
sm.transition('success'); // true
sm.transition('error'); // false (invalid from success)

Conclusion

Private Class Fields bring true encapsulation to JavaScript classes. They provide a language-native way to hide implementation details and create cleaner APIs. While they are still relatively new, they are well-supported in modern browsers and Node.js, making them a valuable tool for writing maintainable and secure JavaScript code.

Use Private Class Fields in your next project to create better encapsulated and more maintainable classes!


Previous Post
Door 22
Next Post
Door 20