skip to content

Access Modifiers & #private Fields

How TypeScript restricts access to class members with public, private, protected and static, and how those compile-time-only rules differ from JavaScript's genuinely private # fields. A favourite interview question because private disappears at runtime while #private does not.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

5

In a TypeScript class, what do the `public`, `private` and `protected` modifiers each control, and which code is allowed to touch a member marked with each?

level: juniorimportance: must knowfreq 70%

answer

  1. the default is the permissive one
  2. declaring class only, versus class plus subclasses
  3. privacy is per class, not per object
  4. a subclass may widen, never tighten
  5. checked by tsc, absent from the emitted JS

basics

~20 s

public, the default, allows access from anywhere. private limits access to the body of the declaring class. protected allows the declaring class and its subclasses. All three are checker-only rules that disappear when TypeScript emits JavaScript.

solid answer

~50 s

A member with no modifier is `public`, so any code holding the instance can read and write it. `private` narrows that to the body of the class that declared it — note that this is per class, not per object, so one instance may reach another instance's private member. `protected` sits between them: the declaring class and any subclass may use it, but outside code may not. The modifiers work the same on methods, accessors and static members, and a `private constructor` blocks construction from outside. The important caveat is that none of this exists at run time: TypeScript erases the modifiers on emit, so they are a design and review tool rather than an enforcement mechanism. A subclass may loosen visibility — re-declaring a `protected` member as `public` — but it may never tighten a `public` member to `private`.

code

typescript · 21 lines
typescript
class Account {
  public id = "acc-1"; // default; the keyword is optional
  protected balance = 0; // declaring class + subclasses
  private auditLog: string[] = []; // declaring class only

  deposit(amount: number): void {
    this.balance += amount;
    this.auditLog.push(`+${amount}`);
  }
}

class SavingsAccount extends Account {
  applyInterest(rate: number): void {
    this.balance *= 1 + rate; // ok: protected
    // this.auditLog.push("interest"); // error: private to Account
  }
}

const a = new SavingsAccount();
console.log(a.id); // ok: public
// a.balance;      // error: protected

go deeper

for a junior

Be able to state the three rules in one sentence each and say that public is the default. Mention that the modifiers vanish when the code compiles to JavaScript - that single caveat separates a prepared answer from a memorised one.

for a middle

Explain the mechanics precisely: privacy is per class rather than per object, protected access from a base-typed reference is rejected, and subclasses may widen but never narrow visibility. Know that constructors and static members take modifiers too.

for a senior

Argue visibility as API surface. Show that protected is a contract with every subclass and is hard to withdraw, that private is the correct default under least privilege, and that none of it survives compilation for JavaScript consumers of a published package.

for a principal

Own the policy question: how much of a class should ever be protected, whether the codebase prefers composition over an inheritance-shaped extension point, and how visibility choices interact with what you publish in .d.ts files and can never quietly take back.

## The three modifiers TypeScript lets you annotate class members — fields, methods, getters/setters and static members alike — with one of three visibility modifiers. They constrain where in your *source* the member may be referenced; the checker enforces them and then the emitter throws them away. ### public `public` is the default, so writing it is optional and mostly a matter of house style. A public member may be read or written by anything holding a reference to the instance. ```ts class User { name = "ada"; // implicitly public public greet() {} // explicitly public, identical meaning } ``` ### private `private` restricts references to the body of the class that declared the member. What surprises people is that the rule is **per class, not per object**: ```ts class Money { private cents: number; constructor(cents: number) { this.cents = cents; } plus(other: Money) { return new Money(this.cents + other.cents); // legal: same class body } } ``` `other.cents` is fine because the reference sits inside `Money`. That is what makes binary operations like `plus`, `equals` and `compareTo` writable without exposing state. ### protected `protected` opens the member to the declaring class *and* its subclasses, while keeping it closed to unrelated code. It is the modifier for a genuine extension point: state or a helper method that derived classes need but consumers must not see. ```ts class Shape { protected sides = 0; } class Square extends Shape { describe() { return `${this.sides} sides`; } // ok } new Square().sides; // error: 'sides' is protected ``` ## The cross-hierarchy rule A subclass may only reach a protected member **through an instance of its own class or a subclass of it**, not through a base-typed reference: ```ts class Base { protected id = 1; } class Child extends Base { read(other: Base) { return other.id; } // error readOwn(other: Child) { return other.id; } // ok } ``` The reason is that `other: Base` might actually be a `Sibling` at run time, and `Child` has no business reading `Sibling`'s protected state. Interviewers like this one because it separates people who have read the rule from people who have hit it. ## Widening, never narrowing A derived class may expose more than its base: re-declaring an inherited `protected` member as `public` is allowed, because every promise the base made still holds. Going the other way — turning a `public` member `private` in a subclass — is rejected, since the subclass must remain usable wherever the base is expected. ## Where they apply All three modifiers work on instance fields, methods, getters and setters, static members (`private static counter = 0`) and constructors. Marking a constructor `private` or `protected` prevents `new` from outside the class, which is the mechanical basis of factory and singleton patterns. ## The erasure caveat Every one of these rules lives in the type layer. The emitted JavaScript contains plain properties and methods; nothing checks visibility at run time, and a JavaScript consumer of your compiled code sees no difference between a public and a private member. Treat the modifiers as documentation the compiler enforces for TypeScript callers — excellent for keeping a codebase honest, useless as a security boundary. If you need a member that is genuinely unreachable after compilation, that is what a `#name` field is for. ## Choosing under least privilege Start every member `private` and widen only when a caller genuinely needs it. Prefer `private` to `protected` unless you are deliberately designing for inheritance: a `protected` member is part of your contract with every subclass, and taking it back later is a breaking change for them even though your public API never moved.

  • Inside a subclass, why does reading a protected member off a parameter typed as the base class fail?
    Because a base-typed reference might hold a sibling subclass at run time, and protected access is meant to stay within your own branch of the hierarchy. TypeScript therefore only allows protected access through an instance of the accessing class or a subclass of it. Typing the parameter as the derived class fixes it.
  • Can a subclass change the visibility of an inherited member?
    It can loosen it — re-declaring a `protected` member as `public` is allowed, since the subclass still honours every guarantee the base made. It cannot tighten a `public` member to `private` or `protected`, because that would break substitutability: code holding the base type would expect the member to be there.
  • Does `private` stop one instance from reading another instance's field?
    No. TypeScript's privacy is per class, so any code inside the declaring class body may read `other.field` on another instance of that same class. This is what makes methods like `equals`, `compareTo` and arithmetic combinators possible without exposing state publicly.

saying these in an interview costs you the question

  • Says members are private by default
  • Claims private blocks access from another instance of the same class
  • Thinks protected means visible to the whole module or package
  • Believes the modifiers are enforced when the code runs
  • Says a subclass can tighten a public member to private

context

open as a page

In TypeScript, what does marking a class member `private` actually prevent, and how is that different from declaring the member as `#name`?

level: middleimportance: must knowfreq 80%

basics

~20 s

TypeScript's private is a compile-time-only check that is erased on emit, so the property still exists and plain JavaScript can read it. A #name field is a real ECMAScript private field that stays unreachable from outside the class at run time.

open as a page

In TypeScript, what does marking a class constructor `private` do, and how does a `protected constructor` differ?

level: middleimportance: should knowfreq 40%

basics

~20 s

A private constructor stops outside code from writing new C() and also stops the class from being extended, leaving static factory methods inside the class as the only way to build instances. A protected constructor blocks direct new but still permits subclasses.

open as a page

Code outside a TypeScript class writes `instance['secret']` to read a member declared `private secret: string`. Does that compile, and why does TypeScript allow it?

level: middleimportance: should knowfreq 36%

basics

~20 s

Yes, it compiles. TypeScript applies the private check to dot access only; element access with a string literal is a deliberate escape hatch, and it still resolves to the member's declared type rather than any.

open as a page

A TypeScript service class stores an API token in a field declared `private token: string`, and the token keeps turning up in log lines and JSON responses. Explain how that happens and what change actually stops it.

level: seniorimportance: should knowfreq 33%

basics

~20 s

The private modifier is erased at compile time, so the token is an ordinary enumerable property that JSON.stringify, object spread and structured loggers all pick up. Moving it to a #token field, or adding a toJSON that omits it, removes it from that output.

open as a page