AniUI Academy

Classes and Access Modifiers

public, private, and protected members, readonly fields, parameter properties, and what TypeScript's private actually protects compared to JavaScript's own

10 min read

Classes in TypeScript are ordinary JavaScript classes with type annotations and access control layered on top. If you already know JavaScript classes, the syntax here should feel familiar — what's new is what the compiler enforces around it.

public, private, and protected

Three visibility modifiers control where a member can be accessed from:

class BankAccount {
  public accountHolder: string; // accessible from anywhere (the default)
  private balance: number;      // only inside BankAccount itself
  protected accountNumber: string; // BankAccount and its subclasses
 
  constructor(accountHolder: string, accountNumber: string) {
    this.accountHolder = accountHolder;
    this.balance = 0;
    this.accountNumber = accountNumber;
  }
 
  deposit(amount: number) {
    this.balance += amount; // fine — inside the declaring class
  }
}
 
const account = new BankAccount("Amara", "123");
account.accountHolder;   // fine — public
account.balance;
// Property 'balance' is private and only accessible within class 'BankAccount'.

With no modifier written at all, a member is public by default — the same as writing it explicitly. private and protected must be opted into.

private vs protected in a subclass

The distinction only shows up once inheritance is involved:

class SavingsAccount extends BankAccount {
  addInterest() {
    this.accountNumber; // fine — protected reaches subclasses
    this.balance;
    // Property 'balance' is private and only accessible within class 'BankAccount'.
  }
}

protected exists specifically for this case: fields a subclass needs to build on, but that outside code still shouldn't touch directly. private is stricter — not even a subclass gets access, only the exact class that declared it.

Compile-time only, same rule as before

This is worth stating plainly, because it surprises people coming from languages where privacy is enforced by the runtime: TypeScript's private and protected are erased during compilation, exactly like every other annotation covered so far. The resulting JavaScript object has an ordinary, fully accessible property:

// After compiling, BankAccount's "private" balance is just a normal property.
const account = new BankAccount("Amara", "123");
(account as any).balance; // reachable at runtime — the type system just won't let you write this without the cast

If you need privacy actually enforced at runtime — not just at compile time — JavaScript's own # private field syntax is the real mechanism, and TypeScript fully supports it:

class BankAccount {
  #balance = 0; // genuinely private at runtime, not just at compile time
 
  deposit(amount: number) {
    this.#balance += amount;
  }
}

#balance cannot be accessed from outside the class under any circumstances, cast or no cast, because the JavaScript engine itself enforces it — not the type checker.

readonly fields

Same meaning as readonly on an interface property: assignable once (at declaration or in the constructor), then locked, at compile time:

class Lesson {
  readonly slug: string;
 
  constructor(slug: string) {
    this.slug = slug; // fine — still inside the constructor
  }
 
  rename(newSlug: string) {
    this.slug = newSlug;
    // Cannot assign to 'slug' because it is a read-only property.
  }
}

Parameter properties

Writing a visibility modifier directly on a constructor parameter is shorthand that both declares the field and assigns it, in one line:

class Lesson {
  constructor(
    public title: string,
    private minutes: number,
    readonly slug: string
  ) {}
}

is exactly equivalent to:

class Lesson {
  public title: string;
  private minutes: number;
  readonly slug: string;
 
  constructor(title: string, minutes: number, slug: string) {
    this.title = title;
    this.minutes = minutes;
    this.slug = slug;
  }
}

Parameter properties are extremely common in real code for exactly this reason — a class with several constructor-assigned fields shrinks considerably, without changing what the class actually does.

Try it yourself

Try it yourself
Loading playground...

What to remember

  • Members are public by default; private restricts access to the declaring class only, protected extends that access to subclasses.
  • TypeScript's private and protected are compile-time only and are erased — genuine runtime privacy needs JavaScript's own #field syntax.
  • readonly on a class field works the same as on an interface: assignable once, then locked, at compile time.
  • A visibility modifier on a constructor parameter (a parameter property) declares and assigns a field in one line.

Check yourself

4 questions · pass 3/4 to unlock Abstract Classes and Polymorphism

up to 50
  1. 1.What is the default access level for a class member with no modifier written?

  2. 2.What is the actual difference between private and protected?

  3. 3.What does constructor(private name: string) {} do, compared to writing private name: string; above the constructor and assigning it manually?

  4. 4.Does TypeScript's private keyword prevent access to that property at runtime, after compiling to JavaScript?

4 left to answer