Access Modifiers: public, private, and protected Explained
Access Modifiers: public, private, and protected Explained
LinkedIn Hook
Here is a question that shows up in nearly every OOP interview: "What is the difference between private and protected?"
Most candidates fumble it. They know private means hidden, but they are fuzzy on protected — and they're not sure how any of this translates to JavaScript, which doesn't have these keywords the same way Java or TypeScript does.
In this lesson you'll learn exactly what each access modifier allows, who can access what from where, and why interviewers care about this topic in the first place. You'll see the same concept expressed in both TypeScript and plain JavaScript so you understand what's native and what's simulated.
Read the full lesson → [link] #OOP #JavaScript #TypeScript #SoftwareEngineering #InterviewPrep
What You'll Learn
public,private, andprotectedcontrol who can read and write a class member from outside the class.privateis accessible only inside the class that defines it — not even subclasses can touch it.protectedopens access to the defining class and all subclasses, but nothing outside.- JavaScript has no native
protectedkeyword. TypeScript adds it at compile time; JavaScript uses conventions and#privateFieldfor true runtime privacy. - Knowing these distinctions is a near-universal OOP interview requirement.
What Are Access Modifiers, and Why Do They Exist?
Access modifiers are keywords that control the visibility of a class member — a property or a method. They answer one question: who is allowed to read or change this piece of data? Without them, every property is fair game for any piece of code anywhere in the codebase. That causes bugs that are difficult to trace, because nothing stops an outside caller from putting an object into an invalid state.
[UNIQUE INSIGHT]: Access modifiers are not primarily a security feature. They are a contract. When you mark a method private, you're telling every future developer: "This is an implementation detail. I reserve the right to change or delete it without warning." When you mark something public, you're committing to keeping that interface stable. Interviews often test whether you understand this design intent, not just the syntax.
[INTERNAL-LINK: understanding encapsulation → Lesson 2.1 — Encapsulation]
The Three Modifiers: a Plain-Language Breakdown
public — Open to Everyone
A public member can be accessed from anywhere: inside the class, from a subclass, and from code completely outside the class hierarchy. In JavaScript classes, every member is public by default unless you explicitly declare it otherwise.
// JavaScript — default is public
class Car {
constructor(make, model) {
this.make = make; // public — accessible from anywhere
this.model = model; // public — accessible from anywhere
}
describe() { // public method
return `${this.make} ${this.model}`;
}
}
const car = new Car("Toyota", "Camry");
// All of these work — Car exposes everything
console.log(car.make); // "Toyota"
console.log(car.model); // "Camry"
console.log(car.describe()); // "Toyota Camry"
// You can even overwrite it from outside — no protection at all
car.make = "Honda";
console.log(car.make); // "Honda" — this is exactly the problem public creates
Public access is the default and the least restrictive. Use it only for members that form the intentional, stable interface of the class.
private — Locked Inside the Class
A private member is accessible only from inside the class that defines it. No outside caller can read it, no subclass can inherit it, and no method on a different object can touch it. In TypeScript, the private keyword enforces this at compile time. In JavaScript (ES2022+), the # prefix enforces it at runtime.
// TypeScript — private keyword (compile-time enforcement)
class BankAccount {
public owner: string;
private balance: number; // only accessible inside BankAccount
constructor(owner: string, initialBalance: number) {
this.owner = owner;
this.balance = initialBalance;
}
deposit(amount: number): void {
if (amount <= 0) return;
this.balance += amount; // allowed — inside the class
}
getBalance(): number {
return this.balance; // allowed — inside the class
}
}
const account = new BankAccount("Alice", 1000);
console.log(account.owner); // "Alice" — public, no problem
console.log(account.getBalance()); // 1000 — allowed through public method
// TypeScript compile error: Property 'balance' is private
// console.log(account.balance);
// JavaScript — # prefix (runtime enforcement, ES2022)
class BankAccount {
#balance; // private field — truly inaccessible outside this class
constructor(owner, initialBalance) {
this.owner = owner; // public
this.#balance = initialBalance;
}
deposit(amount) {
if (amount <= 0) return;
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount("Alice", 1000);
console.log(account.getBalance()); // 1000 — allowed through public method
// Runtime SyntaxError — truly blocked, not just a warning
// console.log(account.#balance);
The #balance field cannot be accessed outside the class at runtime. This is distinct from TypeScript's private, which disappears after compilation. The JavaScript # field is enforced by the engine itself.
[PERSONAL EXPERIENCE]: In code reviews, swapping TypeScript's private keyword for the JavaScript # prefix is a common point of confusion. TypeScript private gives you a compile-time error but becomes a normal property in the compiled JavaScript output. If you need runtime privacy in a JavaScript environment (for example, in a Node.js service without TypeScript), you need the # prefix.
protected — Open to the Family
A protected member is accessible inside the defining class and inside any subclass that extends it. It is not accessible from code outside the class hierarchy. TypeScript supports protected natively. JavaScript has no native equivalent — the _underscore convention signals intent but enforces nothing.
// TypeScript — protected keyword
class Animal {
public name: string;
protected sound: string; // accessible in Animal and all subclasses
constructor(name: string, sound: string) {
this.name = name;
this.sound = sound;
}
makeSound(): string {
return `${this.name} says ${this.sound}`; // allowed — inside class
}
}
class Dog extends Animal {
constructor(name: string) {
super(name, "woof");
}
bark(): string {
// Allowed — Dog is a subclass of Animal
return `${this.name} barks: ${this.sound}!`;
}
}
const dog = new Dog("Rex");
console.log(dog.makeSound()); // "Rex says woof"
console.log(dog.bark()); // "Rex barks: woof!"
// TypeScript compile error: Property 'sound' is protected
// console.log(dog.sound);
The sound property is shared between Animal and Dog through the inheritance chain, but outside code has no direct path to it. That is precisely what protected is designed for: sharing internal state with subclasses without leaking it to the rest of the codebase.
[INTERNAL-LINK: inheritance and class extension → Chapter 4 — Inheritance Deep Dive]
Comparison Table: public vs private vs protected
| Modifier | Inside Defining Class | Inside Subclass | Outside the Class |
|---|---|---|---|
public | Yes | Yes | Yes |
protected | Yes | Yes | No |
private | Yes | No | No |
A quick way to remember the difference: think of rings of access getting smaller. public is the open outside ring. protected is the family ring — the class and its children only. private is the innermost ring — the class alone.
[INTERNAL-LINK: how subclasses access parent members → Lesson 2.5 — How to Access Private & Protected Members]
JavaScript vs TypeScript vs Java: What Is Native and What Is Not?
This comparison is a frequent interview follow-up question. JavaScript, TypeScript, and Java handle access modifiers very differently.
LANGUAGE COMPARISON — Access Modifier Support
-----------------------------------------------------------------
Feature | JavaScript (ES2022+) | TypeScript | Java
-----------------------------------------------------------------
public | Default (implicit) | public | public
private (runtime) | #privateField | No* | private
private (compile) | No | private | private
protected | No native support | protected | protected
Convention only | _underscore | - | -
-----------------------------------------------------------------
* TypeScript's `private` compiles away — it is NOT runtime private
* JavaScript's `#` field is enforced by the JS engine at runtime
[ORIGINAL DATA]: One of the most commonly misunderstood points in frontend interviews is that TypeScript's private keyword does not produce truly private fields at runtime. After compilation, private balance in TypeScript becomes a regular property on the JavaScript object. Any code that bypasses TypeScript's type checker — or simply uses (account as any).balance — can read it freely. Only the # prefix provides real runtime privacy in JavaScript.
Why does Java have full support and JavaScript does not? Java was designed from the start with strict access control as a language feature. JavaScript was originally a scripting language for web pages and had no class system until ES6 (2015). True private fields were not added until ES2022. TypeScript sits in between — it adds compile-time checks that disappear at runtime.
A Real-World Analogy: The Hospital Records System
Imagine a hospital. Think of the hospital as a class.
-
Public members are like the hospital's reception desk. Anyone — patients, visitors, delivery staff — can walk up and interact with it. The phone number on the website, the check-in process, the appointment booking system. All public.
-
Protected members are like the internal staff system. Only hospital employees and doctors at affiliated clinics (subclasses) can access patient records, medication stock levels, and internal schedules. An outside visitor cannot walk back and read files from a doctor's terminal.
-
Private members are like the hospital's internal audit logs. Only the specific department that generated them can read them. Not other departments, not affiliated clinics. Locked to the source.
This analogy maps to code precisely: the public interface is what you expose to the world, protected is what you share with trusted inheritors, and private is what you guard even from those inheritors.
A Complete Example Combining All Three
// TypeScript — combining all three access levels in one class hierarchy
class Employee {
public name: string; // visible to everyone
protected department: string; // visible to Employee and subclasses
private #salary: number; // visible only inside Employee
constructor(name: string, department: string, salary: number) {
this.name = name;
this.department = department;
this.#salary = salary;
}
// Public method — the only way for outside code to read salary
getSalary(): number {
return this.#salary;
}
// Public method — raises salary through controlled logic
giveRaise(amount: number): void {
if (amount > 0) {
this.#salary += amount;
}
}
describe(): string {
// All three are accessible here — inside the defining class
return `${this.name} | ${this.department} | $${this.#salary}`;
}
}
class Manager extends Employee {
private teamSize: number;
constructor(name: string, department: string, salary: number, teamSize: number) {
super(name, department, salary);
this.teamSize = teamSize;
}
managerProfile(): string {
// name is public — accessible anywhere
// department is protected — accessible in subclass
// this.#salary is private — NOT accessible here (would throw)
return `Manager: ${this.name} | Dept: ${this.department} | Team: ${this.teamSize}`;
}
}
const emp = new Employee("Alice", "Engineering", 90000);
const mgr = new Manager("Bob", "Product", 120000, 8);
// Public access — works from anywhere
console.log(emp.name); // "Alice"
console.log(emp.getSalary()); // 90000
// Protected access — blocked outside the class hierarchy
// console.log(emp.department); // TypeScript error
// Private access — blocked everywhere outside Employee
// console.log(emp.#salary); // SyntaxError at runtime
console.log(mgr.managerProfile());
// "Manager: Bob | Dept: Product | Team: 8"
emp.giveRaise(5000);
console.log(emp.getSalary()); // 95000 — change happened through public method
Notice that salary is never directly writable or readable from outside Employee. Outside code must use getSalary() and giveRaise(). This is encapsulation at work: the internal state is protected by a controlled public interface.
[INTERNAL-LINK: getter and setter patterns for controlled property access → Lesson 2.4 — Getters & Setters]
Citation Capsule
Access modifiers (
public,private,protected) are a foundational OOP mechanism controlling member visibility in class hierarchies. In TypeScript,privateenforces compile-time access restrictions, while JavaScript's#prefix (ES2022) enforces runtime privacy enforced by the JavaScript engine itself. Java natively supports all three at both compile and runtime. The key distinction: TypeScript'sprivatedisappears after compilation;#privateFieldin JavaScript does not.
Common Mistakes
-
Confusing TypeScript
privatewith real runtime privacy: TypeScript'sprivatekeyword is a compile-time check only. Aftertsccompiles your code,private balancebecomes a regular JavaScript property. Any code that bypasses the type checker can read it. If you need actual runtime privacy, use#balance. -
Assuming JavaScript supports
protectednatively: It does not. Theprotectedkeyword does not exist in JavaScript. TypeScript adds it, Java has it natively — but in plain JavaScript the only native privacy mechanism is the#prefix. The_underscoreconvention signals "treat this as private" but enforces nothing. -
Using
privatein a base class when the subclass needs access: If a subclass legitimately needs to use a parent class field, mark itprotected, notprivate. Marking itprivateand then exposing it through a public getter is a common workaround, but if the intent is to share within the hierarchy,protectedis the correct choice. -
Over-privatizing everything by default: Not every internal property needs to be private. If a property is part of the class's stable, intentional interface — something subclasses and callers genuinely need — mark it
publicorprotecteddeliberately. Private should mean "this is an implementation detail I may change freely." -
Forgetting that
privatein TypeScript only type-checks, not prevents access viaas any: Casting toanyin TypeScript defeatsprivate. This is why architectural discipline matters — access modifiers alone cannot replace proper system design.
Interview Questions
Q: What is the difference between private and protected?
private restricts access to the defining class only — not even subclasses can use it. protected allows access in the defining class and all its subclasses, but still blocks outside code. If you are building a base class and want subclasses to read or override a member, use protected. If the member is a pure implementation detail that no subclass should ever touch, use private.
Q: Does TypeScript's private keyword make a field truly private at runtime?
No. TypeScript's private is a compile-time restriction. After TypeScript compiles to JavaScript, the field becomes a regular property on the object. Code that casts to any or accesses the property in plain JavaScript can read it freely. For actual runtime privacy in JavaScript, the #privateField syntax (ES2022) is the only mechanism enforced by the engine.
Q: JavaScript doesn't have a protected keyword. How do you handle protected-style members?
JavaScript has no native protected. In plain JavaScript, the common convention is the _underscore prefix (this._sound) to signal "this is internal, do not use it outside this class." It is a convention only — nothing enforces it. TypeScript adds real protected enforcement at compile time. If you need cross-language understanding in an interview, mention that TypeScript solves this for the type-checking step, but JavaScript itself does not.
Q: What is the default access level for class members in JavaScript?
Public. Every property and method defined on a JavaScript class is publicly accessible unless you explicitly use the # private field syntax. There is no public keyword in JavaScript — it is simply the default.
Q: Can a subclass access a private member of its parent class?
No. A private member is strictly scoped to the class that defines it. A subclass cannot access it directly, not even through super. If the parent class wants to share something with subclasses, it should be protected. If it must remain completely hidden even from subclasses, private is correct and the subclass must use the parent's public interface to interact with that data.
Quick Reference — Cheat Sheet
ACCESS MODIFIER COMPARISON
---------------------------------------------------------------
Modifier | Same Class | Subclass | Outside Code
---------------------------------------------------------------
public | Yes | Yes | Yes
protected | Yes | Yes | No
private | Yes | No | No
---------------------------------------------------------------
JAVASCRIPT — NATIVE PRIVACY (ES2022)
---------------------------------------------------------------
class Foo {
#secret = 42; // private: # prefix, enforced at runtime
getSecret() {
return this.#secret; // allowed — inside class
}
}
const foo = new Foo();
foo.getSecret(); // 42 — OK
foo.#secret; // SyntaxError — blocked by the engine
TYPESCRIPT — COMPILE-TIME MODIFIERS
---------------------------------------------------------------
class Bar {
public name: string; // accessible everywhere
protected type: string; // accessible in class + subclasses
private id: number; // accessible in class only (compile-time)
#realPrivate: number; // accessible in class only (runtime)
}
NOTE: TypeScript `private` compiles away.
JavaScript `#` stays private at runtime.
CONVENTION ONLY (NO ENFORCEMENT)
---------------------------------------------------------------
this._internalValue // underscore = "please don't use this"
// nothing stops outside code from using it
LANGUAGE SUPPORT AT A GLANCE
---------------------------------------------------------------
JavaScript : public (default), #private (runtime)
TypeScript : public, private (compile), protected (compile)
Java : public, private, protected (all runtime-enforced)
---------------------------------------------------------------
Previous: Lesson 2.1 - Encapsulation → Next: Lesson 2.3 - Private in JavaScript →
This is Lesson 2.2 of the OOP Interview Prep Course — 8 chapters, 41 lessons.