Encapsulation & Access Modifiers
Intermediate · 20 min read · ▶ live playground · ✦ checkpoint
Access modifiers tell TypeScript which parts of a class are meant for callers and which parts are internal. public is the outside API, private marks a member as internal for ordinary TypeScript access, protected is class-and-subclass, readonly means assigned during initialization and not reassigned later, and implements makes a class prove it matches an interface.
This lesson is about encapsulation: keeping the useful surface small so callers do not depend on details you want freedom to change. TypeScript checks that boundary at compile time. JavaScript #private is different: it is runtime-enforced JavaScript syntax, not just a TypeScript access rule.
public and private
Class members are public by default. You can write public when it helps readers see the intended API, but name: string and public name: string mean the same access level.
private means normal TypeScript property access is allowed inside the class body and rejected from outside. Callers use public methods instead.
The field visits is real state on the instance, but it is not part of the public contract. That gives SessionCounter room to change how visits are stored later without forcing every caller to update. Callers can read summary() because that is the class's public surface.
This is the same boundary rule you already use for functions: expose the promise callers should depend on; keep the working parts inside.
TypeScript private versus JavaScript #private
Here is the usually skipped distinction: TypeScript's private and JavaScript's #private protect different worlds.
private is a TypeScript checker rule for ordinary property access. It rejects code like counter.visits from outside the class, but it does not create a special hidden runtime slot.
class SoftSecret {
private code: string;
constructor(code: string) {
this.code = code;
}
reveal(): string {
return this.code;
}
}
console.log(new SoftSecret("red").reveal());→ red
The emitted JavaScript has an ordinary field named code:
class SoftSecret {
code;
constructor(code) {
this.code = code;
}
reveal() {
return this.code;
}
}
console.log(new SoftSecret("red").reveal());JavaScript #private is different. The #code name is part of JavaScript itself, and outside code cannot access that private identifier.
class HardSecret {
#code: string;
constructor(code: string) {
this.#code = code;
}
}
const box = new HardSecret("blue");
console.log(box.#code);→ playground.ts:10:17 - error TS18013: Property '#code' is not accessible outside class 'HardSecret' because it has a private identifier.
Use TS private when your goal is a clear TypeScript API boundary for ordinary class use. Reach for #private when runtime privacy of the field access itself matters. Many application classes use TS private because the team mostly needs the checker and editor to protect the design.
protected, briefly
protected is like private, except subclasses may use the member too. That means the smallest honest example needs a subclass. This is only a preview of inheritance; 6.3 teaches extends, super, and overriding properly.
BonusAccount can touch this.balanceCents because it is inside a subclass. The outside caller still cannot. If you do not have a subclass relationship, protected is usually the wrong access level; use private or a public method instead.
readonly and implements
readonly class fields follow the same rule as readonly object properties from Section 5: the checker prevents reassignment through that property, but it does not freeze the runtime object. In a class, a readonly instance field can be assigned in its initializer or constructor, then it is locked from later assignment.
implements is a compile-time checklist for the class's public instance shape. It says: "this class instance must match this interface." It does not create a runtime marker, and plain objects still use structural typing as before.
The class passes implements PrintableReceipt because instances have a public label(): string method. The private totalCents field is an implementation detail; the interface does not need to know it exists.
When the checklist fails, TypeScript points at the class declaration:
interface Printable {
label(): string;
}
class BrokenReceipt implements Printable {
readonly id: string;
constructor(id: string) {
this.id = id;
}
}→ playground.ts:5:7 - error TS2420: Class 'BrokenReceipt' incorrectly implements interface 'Printable'. Property 'label' is missing in type 'BrokenReceipt' but required in type 'Printable'.
That is the whole point of implements: fail at the class boundary instead of discovering much later that the class does not satisfy the shape you promised.
Next lesson stays with classes but moves into real inheritance: extends, super, override, and abstract classes.
Checkpoint
Answer all three to mark this lesson complete