Private class fields use the # prefix for true encapsulation. Here's how to use them effectively.
Basic Syntax#
1class Counter {
2 // Private field
3 #count = 0;
4
5 increment() {
6 this.#count++;
7 }
8
9 decrement() {
10 this.#count--;
11 }
12
13 getCount() {
14 return this.#count;
15 }
16}
17
18const counter = new Counter();
19counter.increment();
20counter.increment();
21console.log(counter.getCount()); // 2
22
23// Cannot access private field from outside
24console.log(counter.#count); // SyntaxError
25console.log(counter['#count']); // undefinedPrivate Methods#
1class BankAccount {
2 #balance = 0;
3 #transactionHistory = [];
4
5 // Private method
6 #logTransaction(type, amount) {
7 this.#transactionHistory.push({
8 type,
9 amount,
10 timestamp: new Date(),
11 balance: this.#balance,
12 });
13 }
14
15 // Private validation
16 #validateAmount(amount) {
17 if (typeof amount !== 'number' || amount <= 0) {
18 throw new Error('Invalid amount');
19 }
20 }
21
22 deposit(amount) {
23 this.#validateAmount(amount);
24 this.#balance += amount;
25 this.#logTransaction('deposit', amount);
26 }
27
28 withdraw(amount) {
29 this.#validateAmount(amount);
30 if (amount > this.#balance) {
31 throw new Error('Insufficient funds');
32 }
33 this.#balance -= amount;
34 this.#logTransaction('withdrawal', amount);
35 }
36
37 getBalance() {
38 return this.#balance;
39 }
40
41 getHistory() {
42 // Return copy to prevent external modification
43 return [...this.#transactionHistory];
44 }
45}Private Static Fields#
1class ApiClient {
2 // Private static field
3 static #baseUrl = 'https://api.example.com';
4 static #instances = 0;
5
6 // Private instance field
7 #token;
8
9 constructor(token) {
10 this.#token = token;
11 ApiClient.#instances++;
12 }
13
14 // Private static method
15 static #buildUrl(endpoint) {
16 return `${ApiClient.#baseUrl}${endpoint}`;
17 }
18
19 async fetch(endpoint) {
20 const url = ApiClient.#buildUrl(endpoint);
21 const response = await fetch(url, {
22 headers: {
23 Authorization: `Bearer ${this.#token}`,
24 },
25 });
26 return response.json();
27 }
28
29 // Public static method accessing private static
30 static getInstanceCount() {
31 return ApiClient.#instances;
32 }
33}
34
35const client = new ApiClient('secret-token');
36console.log(ApiClient.getInstanceCount()); // 1Private Accessors#
1class Temperature {
2 #celsius = 0;
3
4 // Private getter
5 get #kelvin() {
6 return this.#celsius + 273.15;
7 }
8
9 // Private setter
10 set #kelvin(value) {
11 this.#celsius = value - 273.15;
12 }
13
14 // Public interface
15 get celsius() {
16 return this.#celsius;
17 }
18
19 set celsius(value) {
20 if (value < -273.15) {
21 throw new Error('Below absolute zero');
22 }
23 this.#celsius = value;
24 }
25
26 get fahrenheit() {
27 return (this.#celsius * 9) / 5 + 32;
28 }
29
30 set fahrenheit(value) {
31 this.celsius = ((value - 32) * 5) / 9;
32 }
33
34 // Internal use of private accessor
35 convertFromKelvin(kelvin) {
36 this.#kelvin = kelvin;
37 return this.#celsius;
38 }
39}Checking for Private Fields#
1class MyClass {
2 #privateField = 42;
3
4 // Check if object has private field
5 static hasPrivateField(obj) {
6 try {
7 // Accessing private field throws if not present
8 return #privateField in obj;
9 } catch {
10 return false;
11 }
12 }
13
14 // Alternative using 'in' operator
15 static isInstance(obj) {
16 return #privateField in obj;
17 }
18}
19
20const instance = new MyClass();
21console.log(MyClass.isInstance(instance)); // true
22console.log(MyClass.isInstance({})); // false
23console.log(MyClass.isInstance(null)); // falsePrivate Fields in Inheritance#
1class Animal {
2 #name;
3
4 constructor(name) {
5 this.#name = name;
6 }
7
8 getName() {
9 return this.#name;
10 }
11}
12
13class Dog extends Animal {
14 #breed;
15
16 constructor(name, breed) {
17 super(name);
18 this.#breed = breed;
19 }
20
21 // Cannot access parent's #name directly
22 // This would be a different #name field
23 describe() {
24 return `${this.getName()} is a ${this.#breed}`;
25 }
26}
27
28const dog = new Dog('Max', 'Labrador');
29console.log(dog.describe()); // 'Max is a Labrador'
30
31// Each class has its own private namespace
32class Cat extends Animal {
33 #name = 'Override'; // Different from Animal's #name
34
35 getCatName() {
36 return this.#name; // Returns 'Override'
37 }
38}Factory Pattern with Privacy#
1class SecureUser {
2 #password;
3 #email;
4 #role;
5
6 // Private constructor
7 constructor() {
8 throw new Error('Use SecureUser.create()');
9 }
10
11 // Factory method
12 static create(email, password, role = 'user') {
13 const instance = Object.create(SecureUser.prototype);
14 instance.#email = email;
15 instance.#password = SecureUser.#hashPassword(password);
16 instance.#role = role;
17 return instance;
18 }
19
20 // Private static method
21 static #hashPassword(password) {
22 // Simple hash for example
23 return btoa(password);
24 }
25
26 verifyPassword(password) {
27 return SecureUser.#hashPassword(password) === this.#password;
28 }
29
30 get email() {
31 return this.#email;
32 }
33
34 hasRole(role) {
35 return this.#role === role;
36 }
37}
38
39const user = SecureUser.create('user@example.com', 'secret123');
40console.log(user.verifyPassword('secret123')); // true
41console.log(user.email); // 'user@example.com'State Machine#
1class TrafficLight {
2 static #STATES = ['red', 'yellow', 'green'];
3 #currentIndex = 0;
4 #intervalId = null;
5
6 #transition() {
7 this.#currentIndex = (this.#currentIndex + 1) % TrafficLight.#STATES.length;
8 this.#notify();
9 }
10
11 #notify() {
12 const event = new CustomEvent('statechange', {
13 detail: { state: this.state },
14 });
15 this.dispatchEvent(event);
16 }
17
18 get state() {
19 return TrafficLight.#STATES[this.#currentIndex];
20 }
21
22 start(interval = 3000) {
23 if (this.#intervalId) return;
24 this.#intervalId = setInterval(() => this.#transition(), interval);
25 }
26
27 stop() {
28 if (this.#intervalId) {
29 clearInterval(this.#intervalId);
30 this.#intervalId = null;
31 }
32 }
33}
34
35// Add EventTarget functionality
36Object.assign(TrafficLight.prototype, EventTarget.prototype);Immutable Configuration#
1class Config {
2 #settings;
3 #frozen = false;
4
5 constructor(initialSettings = {}) {
6 this.#settings = { ...initialSettings };
7 }
8
9 get(key) {
10 return this.#settings[key];
11 }
12
13 set(key, value) {
14 if (this.#frozen) {
15 throw new Error('Config is frozen');
16 }
17 this.#settings[key] = value;
18 return this;
19 }
20
21 freeze() {
22 this.#frozen = true;
23 return this;
24 }
25
26 isFrozen() {
27 return this.#frozen;
28 }
29
30 // Return copy, not reference
31 toObject() {
32 return { ...this.#settings };
33 }
34}
35
36const config = new Config({ debug: true })
37 .set('apiUrl', 'https://api.example.com')
38 .set('timeout', 5000)
39 .freeze();
40
41config.set('debug', false); // Error: Config is frozenObservable with Private State#
1class Observable {
2 #value;
3 #subscribers = new Set();
4
5 constructor(initialValue) {
6 this.#value = initialValue;
7 }
8
9 get value() {
10 return this.#value;
11 }
12
13 set value(newValue) {
14 const oldValue = this.#value;
15 this.#value = newValue;
16 this.#notifySubscribers(newValue, oldValue);
17 }
18
19 #notifySubscribers(newValue, oldValue) {
20 this.#subscribers.forEach((callback) => {
21 callback(newValue, oldValue);
22 });
23 }
24
25 subscribe(callback) {
26 this.#subscribers.add(callback);
27
28 // Return unsubscribe function
29 return () => {
30 this.#subscribers.delete(callback);
31 };
32 }
33}
34
35const count = new Observable(0);
36const unsubscribe = count.subscribe((newVal, oldVal) => {
37 console.log(`Changed from ${oldVal} to ${newVal}`);
38});
39
40count.value = 1; // Changed from 0 to 1
41count.value = 2; // Changed from 1 to 2
42unsubscribe();
43count.value = 3; // No logBest Practices#
Usage:
✓ Use # for true privacy
✓ Keep sensitive data private
✓ Hide implementation details
✓ Use private methods for internals
Patterns:
✓ Validation in private methods
✓ State management
✓ Factory methods
✓ Encapsulated logic
Benefits:
✓ True encapsulation
✓ No naming conventions needed
✓ Cannot be accessed externally
✓ Safe from prototype pollution
Avoid:
✗ Private fields in object literals
✗ Accessing via ['#name']
✗ Exposing private data in methods
✗ Over-privatizing everything
Conclusion#
Private class fields with # provide true encapsulation in JavaScript. They cannot be accessed or modified from outside the class, making them ideal for sensitive data and internal implementation. Use private methods for validation and internal logic, and remember that each class has its own private namespace even in inheritance.