OOP Interview Prep
Encapsulation

How to Access Private & Protected Members

The Right Way and the Wrong Way

LinkedIn Hook

You've hidden a field with #privateField. Good.

But now your subclass needs it. Your test needs it. Your serializer needs it.

So you're tempted to just make it public. Or worse — you reach for a hack.

Here's what senior developers actually do: they know exactly four legitimate ways to access restricted members, and they know exactly when each one is appropriate.

This lesson covers all four patterns, the one JS gotcha that breaks every beginner's inheritance code, and how to answer the inevitable interview question: "When is it acceptable to break encapsulation?"

Read the full lesson → [link]

#OOP #JavaScript #TypeScript #Encapsulation #InterviewPrep


How to Access Private & Protected Members thumbnail


What You'll Learn

  • The four legitimate patterns for accessing private and protected members
  • Why a subclass cannot access a JavaScript #privateField from its parent — and what to do instead
  • How protected works in TypeScript (and why it disappears at runtime)
  • What the "friend pattern" is in JavaScript, and when it's justified
  • How to answer the interview question: "When is breaking encapsulation acceptable?"

Introduction

Private and protected members exist for one reason: controlled access. But "controlled" doesn't mean "inaccessible." Every real-world codebase has legitimate scenarios where restricted data needs to flow outward — to subclasses, to test suites, or to serialization layers. The skill is knowing which door to use rather than kicking through a wall.

[INTERNAL-LINK: access modifiers overview → Lesson 2.2: Access Modifiers — public, private, protected]

Key Takeaways

  • Private members are accessible only through the owning class's public methods or getters.
  • JavaScript's #private fields are hard-private: even subclasses cannot reach them, unlike most OOP languages.
  • TypeScript's protected keyword grants inheritance-chain access but is a compile-time guard only.
  • The "friend pattern" in JS uses closures or module scope to grant controlled cross-class access.
  • Breaking encapsulation is justified in exactly three scenarios: testing, serialization, and migration.

The Vault Analogy — Why Access Rules Exist

Think of a class as a bank vault. The #balance field is the cash inside. The teller window is the public interface — deposit(), withdraw(), getBalance(). You never hand customers a vault key. Instead, every interaction passes through a controlled teller who validates, logs, and enforces rules.

Private members work the same way. They're not locked away to be difficult. They're locked so that every read or write goes through logic that protects the object's integrity.

The problem developers hit is that sometimes legitimate actors need access — a subclass, a serializer, a unit test. The solution is not to remove the lock. The solution is to use the right door.

How to Access Private & Protected Members visual 1


Pattern 1 — The Public Interface (Default Choice)

The most common and safest access pattern is to never expose the private field at all. Instead, the class exposes methods that operate on it. Callers interact entirely through those methods.

class BankAccount {
  #balance;

  constructor(initialBalance) {
    this.#balance = initialBalance;
  }

  // Public method: reads the private field internally, returns a copy
  getBalance() {
    return this.#balance;
  }

  // Public method: modifies the private field through controlled logic
  deposit(amount) {
    if (amount <= 0) throw new Error("Amount must be positive");
    this.#balance += amount;
  }

  withdraw(amount) {
    if (amount > this.#balance) throw new Error("Insufficient funds");
    this.#balance -= amount;
  }
}

const account = new BankAccount(500);
account.deposit(200);
console.log(account.getBalance()); // Output: 700

// This throws a SyntaxError — private field is not accessible from outside
// console.log(account.#balance);

The field #balance never leaves the class. The caller gets only what the class decides to share. This is the default pattern you should reach for first.

[INTERNAL-LINK: public methods and behavior → Lesson 2.1: Encapsulation]


Pattern 2 — Getters and Setters (Computed or Validated Access)

When the access needs validation logic, transformation, or computed output, getters and setters are the right tool. They look like property access to the caller but run method logic internally.

[INTERNAL-LINK: full getter/setter coverage → Lesson 2.4: Getters & Setters]

class Temperature {
  #celsius;

  constructor(celsius) {
    this.#celsius = celsius;
  }

  // Getter: reads private field, returns computed value
  get fahrenheit() {
    return this.#celsius * 9 / 5 + 32;
  }

  // Setter: validates before writing to private field
  set celsius(value) {
    if (value < -273.15) {
      throw new RangeError("Temperature cannot go below absolute zero");
    }
    this.#celsius = value;
  }

  get celsius() {
    return this.#celsius;
  }
}

const temp = new Temperature(100);
console.log(temp.fahrenheit); // Output: 212
console.log(temp.celsius);    // Output: 100

temp.celsius = -10;
console.log(temp.fahrenheit); // Output: 14

// This throws RangeError — setter validates before writing
// temp.celsius = -300;

The private field #celsius is still fully protected. But two controlled doors now exist: one for reading Fahrenheit, one for reading and writing Celsius with validation. The caller never touches the raw field.


Pattern 3 — Protected Members and the Inheritance Chain

This is where JavaScript diverges sharply from most OOP languages, and it's a common interview trap.

The Critical JS Gotcha — Subclasses Cannot Access #private Fields

In Java, C#, or Python, marking a field protected (or using a naming convention) gives subclasses access. In JavaScript, #privateField fields are hard-private. The # syntax is not just a convention. It is enforced by the engine at the syntax level. A subclass genuinely cannot reach a parent's #field, even though it inherits everything else.

class Animal {
  #name; // Hard-private in JavaScript

  constructor(name) {
    this.#name = name;
  }

  getName() {
    return this.#name; // Only Animal's own methods can read this
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  describe() {
    // This FAILS — subclass cannot access parent's #private field
    // return `${this.#name} is a ${this.breed}`; // SyntaxError

    // This WORKS — using the parent's public method instead
    return `${this.getName()} is a ${this.breed}`;
  }
}

const dog = new Dog("Rex", "Labrador");
console.log(dog.describe()); // Output: Rex is a Labrador

The fix is straightforward: expose the data through a public or protected method on the parent (getName()), and call that from the subclass. The #name field itself never needs to be shared.

How TypeScript protected Works

TypeScript adds a protected keyword that does allow subclass access. It's a compile-time access control, not a runtime one — at runtime, the field is just a regular JavaScript property.

// TypeScript — protected allows subclass access at compile time
class Animal {
  protected name: string; // Subclasses can access this, outsiders cannot

  constructor(name: string) {
    this.name = name;
  }
}

class Dog extends Animal {
  private breed: string;

  constructor(name: string, breed: string) {
    super(name);
    this.breed = breed;
  }

  describe(): string {
    // Valid in TypeScript — subclass accesses protected member directly
    return `${this.name} is a ${this.breed}`;
  }
}

const dog = new Dog("Rex", "Labrador");
console.log(dog.describe()); // Output: Rex is a Labrador

// This would fail TypeScript compilation — protected is not public
// console.log(dog.name); // Error: Property 'name' is protected

The important interview point: TypeScript protected is a contract enforced by the compiler, not by the JavaScript runtime. If you compile this to JavaScript and access .name directly on the instance, it will work. There is no hard enforcement at runtime in TypeScript.

How to Access Private & Protected Members visual 2

[ORIGINAL DATA]: In JavaScript's ES2022 specification, #privateField access is enforced via a "brand check" — the runtime verifies the receiving object was constructed by the owning class. This means even reflection techniques like Object.getOwnPropertyNames() cannot retrieve a #field name or value, unlike _convention private fields which are regular properties with a naming hint only.


Pattern 4 — The Friend Pattern (Module-Scoped Controlled Access)

Some situations require two classes to share internal state without making that state fully public. JavaScript does not have a friend keyword like C++, but the same effect is achievable through closures and module scope.

The pattern grants one specific class access to another's internals by sharing a key or accessor function through closure, rather than opening the field to everyone.

// Friend pattern via shared WeakMap — internal to this module
const _balance = new WeakMap();

class BankAccount {
  constructor(balance) {
    _balance.set(this, balance); // Store private data in WeakMap
  }

  getBalance() {
    return _balance.get(this);
  }

  deposit(amount) {
    _balance.set(this, _balance.get(this) + amount);
  }
}

class Auditor {
  // Auditor lives in the same module, so it can access _balance directly
  // This is intentional: Auditor is a "friend" of BankAccount
  generateReport(account) {
    const balance = _balance.get(account); // Direct internal access
    return `Audit report: Current balance is ${balance}`;
  }
}

const account = new BankAccount(1000);
account.deposit(500);

const auditor = new Auditor();
console.log(auditor.generateReport(account)); // Output: Audit report: Current balance is 1500

// Outside this module, _balance is not exported — no external access

The key here is that _balance is not exported from the module. Any code outside this file has no way to reach it. The friendship is scoped. This is different from making the field public.

[PERSONAL EXPERIENCE]: In practice, the WeakMap friend pattern is most useful for internal framework code and testing utilities where two tightly coupled classes need to share state without leaking it to the entire application. For application-level code, a well-designed public interface almost always eliminates the need for it.


When Is Breaking Encapsulation Acceptable?

This is a direct interview question. Have a crisp answer ready.

Encapsulation is not about secrecy for its own sake. It's about controlling change. Breaking it means allowing external code to depend on internal details that you may need to change. That creates fragile coupling.

There are three scenarios where relaxing or bypassing encapsulation is genuinely justified.

Testing. Unit tests sometimes need to verify internal state that is never exposed through the public interface. The right approach is to prefer testing through the public API first. If that's not possible, expose a narrow testing accessor — not a full setter. Some teams use a _testOnly_getBalance() naming convention as an explicit signal that the method is not part of the real API.

Serialization and persistence. Saving an object's state to a database or JSON requires reading all fields, including private ones. The standard solution is a dedicated serialization method (toJSON(), serialize()) on the class itself, which controls exactly what gets exported.

Legacy migration. When refactoring old code where everything is already public, a gradual migration may require temporarily accessing fields that should eventually be private. The key is to make this temporary and tracked.

In all three cases, the pattern is the same: the class itself provides the controlled exit point. External code never reaches in directly.

[UNIQUE INSIGHT]: The question "when is breaking encapsulation okay?" is really asking whether you understand the purpose of encapsulation — managing change dependencies — rather than treating it as a syntax rule. Candidates who can articulate this distinction consistently score higher in system design and architecture interviews than those who treat it as a stylistic preference.


Common Mistakes

  • Accessing #privateField in a subclass. This is the most common error JavaScript developers make when coming from Java or Python. In JS, #fields are hard-private. The engine will throw a SyntaxError at parse time, not even at runtime. Use a public or protected method on the parent instead.

  • Treating _underscore as truly private. A field named _balance is still a regular property. Any code can read or write it. The underscore is a convention that signals "don't touch this," but it has zero enforcement. Don't rely on it in security-sensitive code.

  • Confusing TypeScript protected with JavaScript #private. TypeScript protected is a compile-time contract. It disappears in the JavaScript output. If you need runtime enforcement, use #privateField. If you need inheritance access, you need to choose between these two and understand the trade-off.

  • Over-exposing through getters. A getter that returns a mutable object reference gives the caller a backdoor to mutate internal state. Return a copy, a primitive, or an immutable view — not a live reference to an internal array or object.

  • Creating a public setter "just in case." Every setter increases coupling between the class internals and external code. Only expose a setter when the class genuinely supports external writes to that property as part of its contract.


Interview Questions

Q: Can a subclass access a parent's #privateField in JavaScript?

No. JavaScript #private fields use a hard-privacy mechanism enforced at the syntax level. Even subclasses that inherit from the parent class cannot access #fields defined in the parent. A SyntaxError is thrown at parse time, not just at runtime. The correct approach is to expose the needed data through a public method or, in TypeScript, through a protected property.

Q: What is the difference between private and protected in TypeScript?

private restricts access to the declaring class only. protected extends access to the declaring class and all its subclasses. Both are compile-time contracts — they are not enforced at runtime in the compiled JavaScript. For runtime enforcement, you need JavaScript's #privateField syntax instead.

Q: How would you give a test suite access to a private field without making it fully public?

The preferred approach is to test through the public interface. If that's not sufficient, add a narrow accessor method clearly named to signal its test-only purpose, such as _testOnly_getBalance(). Some teams use a WeakMap-based friend pattern within the same module. The key principle is that the class controls its own exit points. No external code should reach in directly.

Q: What happens if you try to call Object.getOwnPropertyNames() on an instance to find its private fields?

Object.getOwnPropertyNames() will not reveal #privateField names. They are stored in a separate internal slot that reflection APIs cannot enumerate. However, it will reveal _underscoreConvention fields, because those are just regular properties with a naming hint. This distinction is important when evaluating the actual security of a privacy pattern.

Q: When is it acceptable to break encapsulation, and what is the right way to do it?

Breaking encapsulation is acceptable in three scenarios: unit testing internal state that cannot be verified through the public API, serializing an object for persistence, and migrating legacy code. In every case, the class itself should provide the controlled access point — a serialize() method, a test accessor, or a migration helper. External code should never reach in directly. The goal of encapsulation is managing change dependencies, not obscurity. Controlled relaxation is acceptable. Uncontrolled access is not.


Quick Reference Cheat Sheet

ACCESS PATTERNS FOR RESTRICTED MEMBERS
---------------------------------------------------------------------
Pattern              When to Use                  How It Works
---------------------------------------------------------------------
Public method        Default choice               Class method reads/writes
                                                  private field internally
Getter / Setter      Computed access, validation  Looks like property access,
                                                  runs method logic
Protected (TS)       Subclass needs direct read   Compile-time only access
                                                  to parent's named property
Friend pattern       Two classes share internals  Shared WeakMap or closure
                     inside a module              scoped to the module
---------------------------------------------------------------------

JAVASCRIPT PRIVATE FIELD — KEY FACTS
---------------------------------------------------------------------
- Syntax: #fieldName (declared in class body, no var/let/const)
- Enforcement: syntax-level, parse-time error if accessed outside owner
- Subclass access: BLOCKED — even child classes cannot read parent #fields
- Reflection: Object.getOwnPropertyNames() does NOT reveal #fields
- Runtime vs compile: enforced at runtime (unlike TypeScript private)
---------------------------------------------------------------------

TYPESCRIPT ACCESS MODIFIER COMPARISON
---------------------------------------------------------------------
Modifier     Owning Class   Subclass   External Code   Runtime Enforced
---------------------------------------------------------------------
public       Yes            Yes        Yes             No (none needed)
protected    Yes            Yes        No              No (compile only)
private      Yes            No         No              No (compile only)
#private     Yes            No         No              YES (JS engine)
---------------------------------------------------------------------

WHEN TO BREAK ENCAPSULATION (and how)
---------------------------------------------------------------------
Scenario             Acceptable Approach
---------------------------------------------------------------------
Unit testing         Add _testOnly_ accessor method on the class itself
Serialization        Add toJSON() or serialize() method on the class
Legacy migration     Temporary accessor, tracked as tech debt
---------------------------------------------------------------------

COMMON MISTAKES
---------------------------------------------------------------------
- Accessing parent.#field in subclass → SyntaxError (hard-private in JS)
- Trusting _underscore as truly private → it's just a convention
- Returning mutable object reference from getter → internal state leak
- Adding setter "just in case" → unnecessary coupling
- Confusing TS private (compile-time) with JS #private (runtime)
---------------------------------------------------------------------

Previous: Lesson 2.4 → Next: Lesson 3.1 →


This is Lesson 2.5 of the OOP Interview Prep Course — 8 chapters, 41 lessons.

On this page