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:
- They are marked with the
#prefix. - They are only accessible within the same class.
- Direct access from outside leads to a syntax error.
- It is not possible to access these fields via normal key access or reflection methods.
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!