// TypeScript: 'balance' is private — direct access is a compile error
// But at runtime, JS has no enforcement without # syntax
class BankAccount {
owner;
balance; // Should be private — TypeScript: private balance: number
constructor(owner, initialBalance) {
this.owner = owner;
this.balance = initialBalance;
}
deposit(amount) {
if (amount <= 0) throw new Error('Amount must be positive');
this.balance += amount;
}
getBalance() {
return this.balance;
}
}
const account = new BankAccount('Alice', 100);
// Bug: directly mutating private field — bypasses validation
account.balance -= 50; // TypeScript error: 'balance' is private
// Runtime: allowed in JS without # syntax
console.log(account.getBalance());
console.log(account.owner);class BankAccount {
owner;
#balance; // True JS private field — runtime enforced with # syntax
constructor(owner, initialBalance) {
this.owner = owner;
this.#balance = initialBalance;
}
deposit(amount) {
if (amount <= 0) return; // guard silently for this demo
this.#balance += amount;
}
withdraw(amount) {
if (amount <= 0 || amount > this.#balance) return; // guard
this.#balance -= amount;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount('Alice', 100);
// Fix: use the public method — validation runs, privacy preserved
account.withdraw(50);
console.log(account.getBalance());
console.log(account.owner);Bug: TypeScript marks balance as private — direct access from outside should be a compile error. Bypassing the deposit/withdraw methods skips validation (e.g., overdraft protection). The fix is to use a withdrawal method.
Explanation: Using # for true JS private fields prevents external access at runtime. The withdraw method contains validation logic — ensuring business rules are always enforced.
Key Insight: TypeScript's private keyword is compile-time only — it's erased at runtime. For true encapsulation, use JavaScript's native #privateField syntax which is enforced at runtime. Always access/mutate private state through public methods that encode business rules.