Classes & Access Modifiers

intermediate classes oop encapsulation

In simple language, access modifiers say “who’s allowed to touch this member”. TypeScript has four: public, private, protected, and readonly. They’re enforced at compile time only — at runtime, JavaScript doesn’t care.

class Account {
  public id: number;          // anyone can read/write
  private balance: number;    // only this class
  protected ownerId: number;  // this class + subclasses
  readonly createdAt: Date;   // anyone can read, no one can write after init

  constructor(id: number, ownerId: number) {
    this.id = id;
    this.balance = 0;
    this.ownerId = ownerId;
    this.createdAt = new Date();
  }
}

Visibility table

modifier
same class
subclass
outside
public
yes
yes
yes
protected
yes
yes
no
private
yes
no
no

public — the default

If we don’t write a modifier, it’s public. Anyone can read and write.

private — class only

Only the class itself can access. Subclasses can’t either. Useful for internal state we don’t want anyone touching.

class Counter {
  private count = 0;
  inc() { this.count++; }
  get value() { return this.count; }
}

const c = new Counter();
c.count; // Error — private

protected — class + subclasses

Same as private, but subclasses can also see it. Common for shared helpers in inheritance hierarchies.

class Animal {
  protected speakVerb = "makes a sound";
  describe() { return `Animal ${this.speakVerb}`; }
}

class Dog extends Animal {
  bark() { return `Dog ${this.speakVerb}`; } // OK — protected reaches subclasses
}

readonly

Orthogonal to the others — can combine with public/private/protected. Means “no reassignment after constructor”.

class Config {
  readonly version = "1.0";
  private readonly secret: string;
  constructor(s: string) { this.secret = s; }
}

TS private vs JS #private

TypeScript’s private is compile-time only — at runtime, the property is just a regular field. JavaScript’s #private (with the hash) is runtime-enforced and totally invisible from outside.

class A {
  private x = 1;     // accessible via (a as any).x at runtime
  #y = 2;            // truly private, even at runtime
}

In interviews, this distinction matters — #y is preferred for real encapsulation in modern code.

Common gotcha

Two private fields with the same name across unrelated classes don’t clash, but TypeScript’s structural typing makes private members nominal — two classes with identical shapes but private fields are NOT assignable to each other. That’s intentional.