Suggest an editImprove this articleRefine the answer for “Access modifiers in TypeScript: public, private, protected, readonly”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Access modifiers in TypeScript** (`public`, `private`, `protected`, `readonly`) define the visibility of class members. `public` allows access from anywhere. `private` restricts access to the defining class only. `protected` extends that to subclasses. `readonly` blocks reassignment after construction but allows reading anywhere. All checks are compile-time only: the emitted JavaScript ignores them entirely. ```typescript class User { public name: string; // anywhere private age: number; // class only protected role: string; // subclasses too readonly id: string; // no reassignment after init } ``` **Key point:** `private` gives no runtime protection. Use JavaScript `#` fields when actual runtime privacy is needed.Shown above the full answer for quick recall.Answer (EN)Image**Access modifiers in TypeScript** (`public`, `private`, `protected`, `readonly`) control which parts of a class are visible from outside, from subclasses, or only from within the class itself. ## Theory ### TL;DR - All class members are `public` by default, so access modifiers mostly *restrict* access, not grant it - `private` locks a member to the defining class only; `protected` opens it to subclasses too; `readonly` lets anyone read but blocks reassignment after construction - These checks exist only at compile time. The emitted JavaScript has no trace of them - Rule of thumb: start with `private`, loosen to `protected` only when inheritance demands it, and mark anything that should not change as `readonly` ### Quick example ```typescript class Animal { public name: string; // Accessible anywhere private secret: string = "hidden"; // This class only protected species: string = "mammal"; // This class + subclasses readonly id: number; // Read anywhere, set once constructor(name: string, id: number) { this.name = name; this.id = id; } } const cat = new Animal("Whiskers", 1); console.log(cat.name); // "Whiskers" - public, works console.log(cat.id); // 1 - readonly, works // cat.secret // Error: private // cat.id = 2; // Error: readonly ``` `public` and `readonly` are reachable outside the class. `private` and `protected` are not. ### Key difference `private`, `protected`, and `readonly` all restrict something, but they restrict different things. `private` restricts *who* can read or write a member (the class itself, nothing else). `protected` relaxes that rule to include subclasses. `readonly` does not restrict who reads at all. It restricts *when* you can write, which is only during construction. These two ideas are independent: you can have a `private readonly` property that is locked to the class and frozen after init. ### When to use - Internal state that no consumer should touch: `private` - A helper method or property a subclass needs to build on: `protected` - An ID, a config URL, anything that must not change after object creation: `readonly` - Anything that is part of the public API: `public` (or just omit the modifier, since it defaults to `public`) ### Comparison table | Modifier | Inside class | Subclass | Outside | Reassign after init | |---|---|---|---|---| | `public` | yes | yes | yes | yes | | `protected` | yes | yes | no | yes | | `private` | yes | no | no | yes | | `readonly` | set once | read only | read only | no | ### How the compiler handles this TypeScript checks these restrictions during the `tsc` build. The compiler scans class declarations and flags any access that violates the declared modifier. After that, the emitted `.js` file contains none of it. V8 and Node.js see plain object properties. So `private balance = 1000` becomes just `balance = 1000` in the output. The TypeScript Language Server in VS Code applies the same rules in real time via `.d.ts` metadata, which is why your editor underlines violations before you run anything. This differs from Java or C#, where access control is enforced at runtime by the VM. In TypeScript, the modifier system is a development tool. If someone casts to `any`, the check disappears. Most developers discover this the first time they log a supposedly private property and see it sitting right there in the console. ### Common mistakes **Mistake 1: trusting `private` to protect sensitive data at runtime** ```typescript class Safe { private pin = 1234; } const s = new Safe(); (s as any).pin = 0000; // Works. No error. console.log((s as any).pin); // 0 ``` `private` erases at compile time. For real runtime hiding, use JavaScript's `#` syntax (ES2022): ```typescript class Safe { #pin = 1234; // V8 physically blocks access even with casting } ``` **Mistake 2: using `protected` when you mean "almost private"** ```typescript class Parent { protected secret = "internal"; } class Child extends Parent { expose() { return this.secret; } } const c = new Child(); c.expose(); // "internal" - it got out ``` If no subclass needs direct access, use `private`. `protected` is for inheritance hooks, not for "slightly less private." **Mistake 3: assuming `readonly` blocks deep mutation** ```typescript class Config { readonly options = { debug: false }; } const cfg = new Config(); cfg.options = { debug: true }; // Error - reference reassignment blocked cfg.options.debug = true; // Fine - mutation of the object itself is allowed ``` `readonly` blocks reassignment of the reference, not mutation of the underlying object. For deep immutability, combine it with `Object.freeze()` or a `DeepReadonly` utility type. **Mistake 4: forgetting parameter property syntax** ```typescript // These two classes behave identically: class User { private name: string; constructor(name: string) { this.name = name; } } class User { constructor(private name: string) {} // Declares and assigns in one step } ``` If you omit the modifier in the constructor signature, the property is never declared on the class. ### Real-world usage - React: `readonly` on prop types to catch mutation attempts inside components - Express/Passport.js: `private` on token handlers and internal middleware state - NestJS: `protected` methods in base controllers that child controllers extend - Redux Toolkit: `readonly` on state slices to catch accidental mutations at the type level - Prisma: `readonly` on generated model IDs ### Follow-up questions **Q:** What does TypeScript `private` compile to in JavaScript? **A:** A plain class field. `class X { private y = 1 }` becomes `class X { y = 1 }` in the output. Any access at runtime works fine. **Q:** Can a subclass access a `private` member? **A:** No. TypeScript gives a compile error. Use `protected` if a subclass needs that member. **Q:** What is the difference between `private` and `readonly`? **A:** They control different things. `private` controls who can access the member. `readonly` controls when you can write to it. A `private readonly` property combines both: class-only access and immutable after construction. **Q:** Why use `private` at all if it has no runtime enforcement? **A:** The benefit is during development. Your IDE flags violations before the code runs, `tsc` blocks invalid builds, and it signals intent to teammates reading the code. **Q:** A library ships to JavaScript consumers. How do you enforce privacy at runtime? **A:** Combine approaches. TypeScript `private` for IDE and type-level feedback. JavaScript `#` fields for actual runtime enforcement: V8 blocks access regardless of casting. For extra isolation, use factory functions that return a typed interface, so consumers never hold a direct reference to the class instance. ## Examples ### All four modifiers in one class ```typescript class BankAccount { public owner: string; // Anyone can read private balance: number; // Internal only protected accountType: string; // Subclasses can read readonly id: string; // Set once, immutable constructor(owner: string, initial: number, id: string) { this.owner = owner; this.balance = initial; this.accountType = "standard"; this.id = id; } public deposit(amount: number): void { this.balance += amount; // OK inside the class } public getBalance(): number { return this.balance; } } const acc = new BankAccount("Alice", 500, "ACC-001"); acc.owner; // "Alice" acc.getBalance(); // 500 // acc.balance; // Error: private // acc.id = "X"; // Error: readonly ``` `balance` is hidden but reachable through `getBalance()`. This is the standard encapsulation pattern for class-based code. ### Inheritance with protected ```typescript class Vehicle { protected speed: number = 0; protected accelerate(amount: number): void { this.speed += amount; } } class Car extends Vehicle { public drive(): string { this.accelerate(60); // OK - protected, subclass can call it return `Speed: ${this.speed}km/h`; } } const car = new Car(); car.drive(); // "Speed: 60km/h" // car.speed; // Error: protected, not part of the public interface ``` `protected` makes sense here because `Car` needs the `Vehicle` internals, but consumers of `Car` should not see them. ### TypeScript private vs JavaScript #private ```typescript class Secure { private tsField = "typescript only"; #jsField = "runtime enforced"; // ES2022 reveal() { console.log(this.tsField); // OK console.log(this.#jsField); // OK } } const s = new Secure(); s.reveal(); (s as any).tsField; // Works - TypeScript private is gone at runtime // s.#jsField; // SyntaxError - V8 itself blocks this ``` The `#` syntax is enforced by V8. TypeScript `private` offers no protection once the code is running. For anything actually sensitive, `#` is the right call.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.