Skip to content
TypeScript ts classes 4 min read

Access Modifiers

Access modifiers control which code is allowed to read or write a class member. TypeScript offers three keywords — public, private, and protected — that enforce encapsulation at compile time. Alongside them, modern JavaScript has its own #private fields that are enforced at runtime. Understanding the difference between the two systems is essential: TypeScript modifiers are erased during compilation, while #private fields are real, hard-private even after the types are gone. This page covers all three modifiers and how they relate to ECMAScript private fields.

public (the default)

A public member is accessible from anywhere. Members are public unless you say otherwise, so the keyword is usually optional and written only for emphasis.

class Counter {
  public count = 0; // explicit
  step = 1;         // implicitly public

  increment(): void {
    this.count += this.step;
  }
}

const c = new Counter();
c.count; // ✅ readable everywhere

Because public is the default, most teams omit the keyword and reserve explicit modifiers for private and protected.

private

A private member is accessible only from within the declaring class. Subclasses and external code cannot touch it. The compiler rejects any outside access.

class BankAccount {
  private balance = 0;

  deposit(amount: number): void {
    this.balance += amount;
  }
  getBalance(): number {
    return this.balance;
  }
}

const acct = new BankAccount();
acct.deposit(100);
acct.getBalance(); // ✅ 100
acct.balance;      // ❌ Error: Property 'balance' is private

This enforces encapsulation: callers interact through deposit and getBalance, never the raw field. Note that TypeScript private is checked only at compile time — at runtime the property still exists as a plain object key.

protected

A protected member is accessible within the declaring class and its subclasses, but not from outside. It is the right choice for state and helpers that derived classes need but consumers should not see.

class Shape {
  protected sides: number;
  constructor(sides: number) {
    this.sides = sides;
  }
}

class Triangle extends Shape {
  constructor() {
    super(3);
  }
  describe(): string {
    return `A shape with ${this.sides} sides`; // ✅ subclass access
  }
}

new Triangle().sides; // ❌ Error: Property 'sides' is protected

Triangle reads this.sides freely because it extends Shape, but external code still cannot.

TypeScript modifiers vs ECMAScript #private

JavaScript itself now supports hard-private fields prefixed with #. These are fundamentally different from TypeScript’s private. A # field is enforced by the language at runtime, is not enumerable, and is genuinely inaccessible outside the class — even via bracket access or casting to any.

class Vault {
  #secret = "hunter2";          // runtime-private (ECMAScript)
  private legacySecret = "abc"; // compile-time-private (TypeScript)

  reveal(): string {
    return this.#secret;
  }
}

const v = new Vault();
(v as any).legacySecret; // ✅ "abc" — TS private is erased, accessible at runtime
(v as any)["#secret"];   // ❌ undefined — # fields are truly hidden
v.#secret;               // ❌ SyntaxError outside the class

The casting trick exposes the TypeScript private field because that modifier disappears after compilation. The #secret field survives because it is a real language feature.

AspectTS private / protectedECMAScript #field
EnforcedCompile time onlyRuntime (and compile time)
Survives as anyNo — accessibleYes — still hidden
Subclass accessprotected allows itNever (always class-only)
Visible in JS outputYes (plain property)Yes (true private slot)
Name collisionsShares normal namespaceSeparate # namespace

Reach for #private fields when you need real runtime encapsulation — for example in security-sensitive code or published libraries. Use TypeScript private/protected when compile-time checking is enough and you want protected semantics, which # cannot express.

Combining with other modifiers

Access modifiers compose with readonly, static, and parameter properties. Order matters: the access modifier comes first.

class Service {
  private static instance: Service;
  public readonly id = crypto.randomUUID();

  private constructor() {}

  static getInstance(): Service {
    return (this.instance ??= new Service());
  }
}

A private constructor blocks new Service() from outside, forcing construction through the static factory — a common singleton technique.

Best Practices

  • Default to the most restrictive modifier that works; start private and widen only when needed.
  • Omit the redundant public keyword except on parameter properties where it is required.
  • Use protected for members subclasses legitimately need, not as a looser private.
  • Prefer #private fields when you need true runtime privacy or are shipping a library.
  • Remember TS private is erased — never rely on it to hide secrets at runtime.
  • Combine private static with a private constructor to implement singletons.
  • Expose state through methods or accessors rather than public mutable fields.
Last updated June 29, 2026
Was this helpful?