In simple language, access modifiers say “who’s allowed to touch this member”. TypeScript has four: public, private, protected, and readonly. They’re enforced at compile time only — at runtime, JavaScript doesn’t care.
class Account {
public id: number; // anyone can read/write
private balance: number; // only this class
protected ownerId: number; // this class + subclasses
readonly createdAt: Date; // anyone can read, no one can write after init
constructor(id: number, ownerId: number) {
this.id = id;
this.balance = 0;
this.ownerId = ownerId;
this.createdAt = new Date();
}
}
Visibility table
public — the default
If we don’t write a modifier, it’s public. Anyone can read and write.
private — class only
Only the class itself can access. Subclasses can’t either. Useful for internal state we don’t want anyone touching.
class Counter {
private count = 0;
inc() { this.count++; }
get value() { return this.count; }
}
const c = new Counter();
c.count; // Error — private
protected — class + subclasses
Same as private, but subclasses can also see it. Common for shared helpers in inheritance hierarchies.
class Animal {
protected speakVerb = "makes a sound";
describe() { return `Animal ${this.speakVerb}`; }
}
class Dog extends Animal {
bark() { return `Dog ${this.speakVerb}`; } // OK — protected reaches subclasses
}
readonly
Orthogonal to the others — can combine with public/private/protected. Means “no reassignment after constructor”.
class Config {
readonly version = "1.0";
private readonly secret: string;
constructor(s: string) { this.secret = s; }
}
TS private vs JS #private
TypeScript’s private is compile-time only — at runtime, the property is just a regular field. JavaScript’s #private (with the hash) is runtime-enforced and totally invisible from outside.
class A {
private x = 1; // accessible via (a as any).x at runtime
#y = 2; // truly private, even at runtime
}
In interviews, this distinction matters — #y is preferred for real encapsulation in modern code.
Common gotcha
Two private fields with the same name across unrelated classes don’t clash, but TypeScript’s structural typing makes private members nominal — two classes with identical shapes but private fields are NOT assignable to each other. That’s intentional.