OOP Interview Prep
Encapsulation

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


Access Modifiers: public, private, and protected Explained thumbnail


What You'll Learn

  • public, private, and protected control who can read and write a class member from outside the class.
  • private is accessible only inside the class that defines it — not even subclasses can touch it.
  • protected opens access to the defining class and all subclasses, but nothing outside.
  • JavaScript has no native protected keyword. TypeScript adds it at compile time; JavaScript uses conventions and #privateField for 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.

Access Modifiers: public, private, and protected Explained visual 1


[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]


Access Modifiers: public, private, and protected Explained visual 2


Comparison Table: public vs private vs protected

ModifierInside Defining ClassInside SubclassOutside the Class
publicYesYesYes
protectedYesYesNo
privateYesNoNo

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.

Access Modifiers: public, private, and protected Explained visual 3


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, private enforces 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's private disappears after compilation; #privateField in JavaScript does not.


Common Mistakes

  • Confusing TypeScript private with real runtime privacy: TypeScript's private keyword is a compile-time check only. After tsc compiles your code, private balance becomes 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 protected natively: It does not. The protected keyword 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 _underscore convention signals "treat this as private" but enforces nothing.

  • Using private in a base class when the subclass needs access: If a subclass legitimately needs to use a parent class field, mark it protected, not private. Marking it private and then exposing it through a public getter is a common workaround, but if the intent is to share within the hierarchy, protected is 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 public or protected deliberately. Private should mean "this is an implementation detail I may change freely."

  • Forgetting that private in TypeScript only type-checks, not prevents access via as any: Casting to any in TypeScript defeats private. 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.

On this page