public, private, protected), while modern JavaScript offers native, runtime-enforced private fields (#).1. The Access Modifier Visibility Matrix#
TypeScript provides three access keywords that control the visibility of class fields, methods, getters, setters, and constructors:
| Access Modifier | Inside Defining Class | Inside Subclasses (extends) | Outside Instances (new) |
|---|---|---|---|
public (Default) | ✅ Allowed | ✅ Allowed | ✅ Allowed |
protected | ✅ Allowed | ✅ Allowed | ❌ Blocked by Compiler |
private | ✅ Allowed | ❌ Blocked by Compiler | ❌ Blocked by Compiler |
2. In Action: public, protected, and private#
Let’s model a banking domain to observe how each modifier operates across base classes, subclasses, and external callers:
class BankAccount {
// 1. PUBLIC: Accessible everywhere (Default visibility if omitted)
public readonly accountNumber: string;
// 2. PROTECTED: Accessible inside BankAccount AND any subclasses (like SavingsAccount)
protected accountStatus: "active" | "frozen" | "closed";
// 3. PRIVATE: Accessible ONLY inside BankAccount methods
private balance: number;
constructor(accountNumber: string, initialBalance: number) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
this.accountStatus = "active";
}
public deposit(amount: number): number {
if (this.accountStatus === "frozen") {
throw new Error("Cannot deposit to a frozen account.");
}
this.balance += amount;
return this.balance;
}
private logTransaction(type: string, amount: number) {
console.log(`[AUDIT]: ${type} of $${amount} on account ${this.accountNumber}`);
}
}
// Subclass extending BankAccount
class SavingsAccount extends BankAccount {
private interestRate: number;
constructor(accountNumber: string, initialBalance: number, interestRate: number) {
super(accountNumber, initialBalance);
this.interestRate = interestRate;
}
public applyInterest() {
// 🟢 WORKED: Can access 'protected' member 'accountStatus' from parent class!
if (this.accountStatus !== "active") {
throw new Error("Account is not active.");
}
// ❌ Compiler Error: Property 'balance' is private and only accessible within class 'BankAccount'.
// this.balance += this.balance * this.interestRate;
}
}External Caller Checks#
const account = new BankAccount("ACC_1001", 500);
// 1. PUBLIC: Accessible externally
console.log(account.accountNumber); // "ACC_1001"
account.deposit(250); // 750
// 2. PROTECTED: Blocked externally!
// ❌ Compiler Error: Property 'accountStatus' is protected and only accessible within class 'BankAccount' and its subclasses.
// account.accountStatus = "frozen";
// 3. PRIVATE: Blocked externally!
// ❌ Compiler Error: Property 'balance' is private and only accessible within class 'BankAccount'.
// console.log(account.balance);
3. Compile-Time Soft Privacy vs Runtime Hard Privacy#
A common misconception among developers transitioning to TypeScript is assuming private makes a property truly secret at runtime.
Soft Privacy Rule: TypeScript’s private and protected keywords are Soft Privacy assertions. They exist only at compile time and undergo Type Erasure during compilation.
The JavaScript Emission Reality#
When tsc compiles BankAccount to JavaScript, the access keywords are completely erased:
// Compiled JavaScript Output (dist/BankAccount.js)
class BankAccount {
constructor(accountNumber, initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance; // Plain JavaScript property!
this.accountStatus = "active";
}
deposit(amount) { ... }
}If a plain JavaScript consumer imports your library, or if an engineer uses dynamic bracket notation (account["balance"]), they can read and modify private properties at runtime:
// Bypassing TypeScript compile-time check using bracket notation:
const secretBalance = (account as any)["balance"]; // Returns 750 at runtime!
4. Hard Private Fields (#privateField)#
To solve the limitations of soft privacy, ECMAScript (ES2022) introduced Native Private Fields, designated by a # prefix.
Unlike TypeScript’s private keyword, # fields provide Hard Privacy enforced natively by JavaScript runtime engines (V8, SpiderMonkey, JavaScriptCore).
class SecureVault {
// Hard private field (Native ES2022 feature)
#masterEncryptionKey: string;
constructor(key: string) {
this.#masterEncryptionKey = key;
}
public getMaskedKey(): string {
return `***${this.#masterEncryptionKey.slice(-4)}`;
}
}
const vault = new SecureVault("sk_live_998811223344");
console.log(vault.getMaskedKey()); // "***3344"
// ❌ SyntaxError: Property '#masterEncryptionKey' is not accessible outside class 'SecureVault'.
// Even if you try bracket notation or 'as any', JavaScript's V8 engine CRASHES at runtime:
// (vault as any)["#masterEncryptionKey"]; // Returns undefined!
// (vault as any).#masterEncryptionKey; // Hard Syntax Error!
5. Comparison: private vs # Private Fields#
| Feature | private field (TypeScript) | #field (ES2022 Native) |
|---|---|---|
| Enforcement Layer | Compile-Time (TypeScript) | Runtime (Browser/Node Engine) |
| Subclass Accessibility | Inaccessible in subclasses | Inaccessible in subclasses |
Bracket Bypass (obj["prop"]) | Bypassed at runtime | Impossible (Returns undefined or syntax error) |
| Compiled Output | Standard property this.prop | Preserves #prop (or uses WeakMap polyfills) |
| Performance Overhead | Zero (Plain property) | Microscopic (WeakMap lookup overhead in older targets) |
| Declaration Syntax | private balance: number | #balance: number |
Summary & Next Steps#
In this episode:
- We analyzed the Visibility Matrix for
public,protected, andprivatemembers. - We demonstrated inheritance rules (
protectedis accessible in subclasses,privateis not). - We uncovered Soft Privacy: TypeScript access modifiers undergo type erasure and do not prevent runtime access.
- We implemented Hard Privacy using native ES2022
#privateFieldsyntax.
In Episode 18: Parameter Properties, we will learn TypeScript’s shorthand syntax for declaring and initializing constructor fields in a single line!

