Private Fields and Static Members
Two features round out modern class syntax: private fields for encapsulating internal data, and static members for attaching properties and methods to the class itself.
Private fields
Section titled “Private fields”By default, every property on a class instance is public — any code that has a reference to the instance can read or write it. Sometimes you want to protect internal state from outside interference.
Private fields are declared with a # prefix. They can only be accessed from inside the class body:
class Expense { #id; #createdAt;
constructor({ description, amount, category, date }) { this.#id = crypto.randomUUID(); this.#createdAt = Date.now(); this.description = description.trim(); this.amount = amount; this.category = category ?? 'Uncategorized'; this.date = date ?? new Date().toISOString().split('T')[0]; }
get id() { return this.#id; }
get createdAt() { return this.#createdAt; }}
const coffee = new Expense({ description: 'Coffee', amount: 4.50, category: 'Food' });
console.log(coffee.id); // the UUID — accessible via getterconsole.log(coffee.#id); // SyntaxError — private field, inaccessible from outside#id cannot be read or written from outside the class. The only way to access it is through the id getter, which you control.
Getters
Section titled “Getters”A getter is a method that is accessed like a property — no parentheses needed:
get id() { return this.#id;}
console.log(coffee.id); // called like a property, not coffee.id()Getters are useful for computed or read-only properties. You can also define setters that run when a property is assigned:
set amount(value) { if (typeof value !== 'number' || value <= 0) { throw new Error('Amount must be a positive number'); } this.amount = value;}Static properties
Section titled “Static properties”A static property belongs to the class, not to instances. It is useful for constants and class-level configuration:
class Expense { static CATEGORIES = ['Food', 'Transport', 'Health', 'Entertainment', 'Other'];
// ...}
console.log(Expense.CATEGORIES); // ['Food', 'Transport', ...]console.log(coffee.CATEGORIES); // undefined — not on instancesExpense.CATEGORIES is available anywhere you have access to the Expense class — useful for populating a category dropdown or validating input.
Static methods revisited
Section titled “Static methods revisited”You saw fromJSON as a static method in lesson 03. Static methods are also useful for utility operations that relate to the class but do not need an instance:
class Expense { static CATEGORIES = ['Food', 'Transport', 'Health', 'Entertainment', 'Other'];
static isValidCategory(category) { return Expense.CATEGORIES.includes(category); }
static fromJSON(data) { return new Expense(data); }}
console.log(Expense.isValidCategory('Food')); // trueconsole.log(Expense.isValidCategory('Snacks')); // falseThe complete Expense class
Section titled “The complete Expense class”Here is the full Expense class you will use in BudgetBuddy, combining everything from this module:
class Expense { static CATEGORIES = ['Food', 'Transport', 'Health', 'Entertainment', 'Other'];
#id; #createdAt;
constructor({ id, description, amount, category, date, createdAt } = {}) { if (!description || description.trim() === '') throw new Error('Description is required'); if (typeof amount !== 'number' || amount <= 0) throw new Error('Amount must be positive');
this.#id = id ?? crypto.randomUUID(); this.#createdAt = createdAt ?? Date.now(); this.description = description.trim(); this.amount = amount; this.category = category ?? 'Other'; this.date = date ?? new Date().toISOString().split('T')[0]; }
get id() { return this.#id; } get createdAt() { return this.#createdAt; }
format() { return `${this.description}: $${this.amount.toFixed(2)}`; }
isInCategory(category) { return this.category.toLowerCase() === category.toLowerCase(); }
toJSON() { return { id: this.#id, description: this.description, amount: this.amount, category: this.category, date: this.date, createdAt: this.#createdAt, }; }
static fromJSON(data) { return new Expense(data); }
static isValidCategory(category) { return Expense.CATEGORIES.includes(category); }}Exercise
Section titled “Exercise”- Add private fields
#idand#createdAtto yourExpenseclass. Expose them withgetaccessors. - Add
static CATEGORIESwith five category strings. - Add
static isValidCategory(category)that checks whether a string is inCATEGORIES. - Confirm that trying to access
expense.#idfrom outside the class throws aSyntaxError. - Confirm that
Expense.CATEGORIESis accessible butexpense.CATEGORIESisundefined.
- Private fields (
#name) can only be accessed inside the class body — they protect internal state. - Getters expose private or computed values as read-only properties.
- Static properties (
static name = value) belong to the class, not instances — useful for constants and configuration. - Static methods (
static methodName()) attach utility logic to the class itself — factory methods, validators, helpers.