Classes & Access Modifiers

Marking a class's properties and methods as `public`, `private`, or `protected` to control what code outside the class is allowed to touch.

What is it?

A JavaScript class can have properties and methods, but every one of them is reachable from outside the class by default — nothing stops other code from reading or overwriting a property that was only ever meant to be an internal implementation detail, like a bank account's raw balance field.

TypeScript adds three access modifiers you can put in front of a class member: public (the default — reachable from anywhere, same as plain JavaScript), private (only reachable from inside the class itself), and protected (reachable from inside the class and any class that extends it, but not from outside). These are enforced by the compiler at compile time — trying to access a private member from outside the class is flagged as an error before the code ever runs.

Explain like I'm 10

Think of a class like a house. public is the front porch — anyone can walk up to it. private is a locked room only the homeowner has a key to. protected is like a family room — the homeowner's kids (subclasses) can use it too, but a random visitor off the street cannot.

Examples

public, private, and enforcement

class BankAccount {
  public accountHolder: string;
  private balance: number;

  constructor(accountHolder: string, startingBalance: number) {
    this.accountHolder = accountHolder;
    this.balance = startingBalance;
  }

  public deposit(amount: number): void {
    this.balance += amount; // fine — inside the class
  }

  public getBalance(): number {
    return this.balance;
  }
}

const account = new BankAccount("Lena", 100);
account.deposit(50);
console.log(account.balance);
// Error: Property 'balance' is private and only accessible within class 'BankAccount'.

balance can only be read or changed from methods defined inside BankAccount. Outside code must go through deposit and getBalance instead of touching the field directly.

protected and inheritance

class Animal {
  protected name: string;

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

class Dog extends Animal {
  bark(): string {
    return `${this.name} says woof!`; // OK — protected is visible in subclasses
  }
}

const dog = new Dog("Rex");
console.log(dog.name);
// Error: Property 'name' is protected and only accessible
// within class 'Animal' and its subclasses.

// A shorthand: declaring and assigning a parameter in one step
class Cat {
  constructor(private lives: number = 9) {}
  loseLife(): number {
    return --this.lives;
  }
}

Dog can use this.name because protected extends visibility to subclasses, but code outside the class hierarchy still can't reach it. The Cat example shows a shorthand where adding a modifier directly to a constructor parameter both declares and assigns the property.

How it works

Access modifiers are checked entirely at compile time — the compiler tracks, for every property and method, which modifier it has and where the accessing code is located (inside the same class, inside a subclass, or fully outside), and reports an error for any access that violates the rule. Once compiled to plain JavaScript, these checks disappear — JavaScript itself (outside of its own newer, unrelated #private field syntax) has no concept of "private," so at runtime, without extra protection, the property is technically still reachable if someone bypasses the type checker entirely (for example from plain, untyped JavaScript calling into the compiled code).

Why does it exist?

As a class grows, it usually ends up with some state that's purely an internal implementation detail — a cache, a counter, a raw value that should only ever be changed through a specific method that keeps it valid. Access modifiers let the class enforce that boundary, so other code is guided toward the intended public methods instead of reaching in and manipulating internal state directly, which keeps the class's guarantees about its own state reliable.

When to use it

Mark a property or method private whenever it's purely an internal detail that outside code has no legitimate reason to touch directly — raw internal state, helper methods that only make sense as steps within a larger public method. Use protected specifically when subclasses need that access too, but unrelated outside code still shouldn't have it.

When not to use it

Don't mark something private just out of habit — if a property is genuinely meant to be read or set freely from outside the class (like a simple data holder with no invariants to protect), public (the default) is the right, simpler choice. Over-restricting access can force callers to write awkward workarounds for legitimate uses.

Common mistakes

  • Assuming private blocks access at runtime in the compiled JavaScript, the same way it's blocked at compile time — it's a compile-time-only check unless you use JavaScript's own #field private syntax.

  • Marking every single property private reflexively, even ones meant to be freely read from outside, forcing unnecessary getter methods for no real benefit.

  • Using protected when private was actually intended, allowing an unrelated subclass to reach in and depend on internal details that were never meant to be part of its contract.

Practice exercises

  1. Easy:

    Write a class Counter with a private count: number field, and increment() and getCount() public methods, then try accessing count directly from outside to see the error.

  2. Medium:

    Write a base class Vehicle with a protected speed: number, and a subclass Car with a method that uses this.speed. Then try accessing speed from outside both classes to confirm it's blocked.

  3. Hard:

    Rewrite the Counter class using constructor-parameter shorthand (constructor(private count: number = 0) {}), and explain in a comment what changed compared to declaring the field separately.

Interview questions

What do `public`, `private`, and `protected` each allow, in terms of what code can access the member?

public (the default) is reachable from anywhere; private is reachable only from inside the class where it's declared; protected is reachable from that class and any class that extends it, but not from unrelated outside code.

What access modifier does a class member get if you don't write one explicitly?

public — it's the default, identical to how a plain JavaScript class member behaves with no restriction at all.

How does the compiler decide whether a given piece of code is allowed to access a class member?

It tracks each member's declared modifier and checks where the accessing code lives relative to the class — inside the declaring class itself, inside a subclass, or fully outside — and flags any access that the member's modifier doesn't permit from that location.

Do access modifiers like `private` and `protected` exist as a runtime concept in the compiled JavaScript?

No — the check is entirely compile-time. Once compiled, the property is a completely ordinary JavaScript property with no built-in restriction, unless the code separately uses JavaScript's own native #field syntax.

Given `class BankAccount { private balance: number; ... }` and `const account = new BankAccount(...); console.log(account.balance);`, why does this fail to compile?

balance is marked private, so it's only accessible from methods declared inside BankAccount itself — the console.log call is outside the class entirely, which the private modifier doesn't permit regardless of what value balance actually holds.

Why does `BankAccount` expose `deposit()` and `getBalance()` as public methods instead of just making `balance` public directly?

Keeping balance private forces every change to go through deposit, which can enforce whatever rules the class needs (e.g. never letting the balance go negative), rather than letting outside code overwrite the raw value directly and possibly leave it in an invalid state.

What problem do access modifiers solve that a plain JavaScript class doesn't address on its own?

In plain JavaScript every class member is reachable from outside by default, with nothing stopping other code from reading or overwriting internal state that was only ever meant to be an implementation detail — access modifiers let TypeScript flag that kind of access as a compile-time error.

What's the difference between `private` and `protected`?

private members are reachable only from inside the exact class that declares them; protected members are additionally reachable from any subclass that extends that class, while both still block access from unrelated outside code.

Given `class Animal { protected name: string; ... }` and `class Dog extends Animal { bark() { return this.name; } }`, why does `this.name` compile fine inside `bark()`?

protected extends visibility to the declaring class and any class that extends it — Dog extends Animal, so code inside Dog's own methods is allowed to read this.name.

Using the same `Animal`/`Dog` classes, why does `dog.name` fail when accessed from completely outside both classes?

protected only opens access to the declaring class and its subclasses' own code — accessing name from outside that hierarchy entirely, like a plain dog.name at the top level, isn't covered by that rule and is rejected the same way private would reject it.

The topic's analogy describes `protected` as being open to "the family" but not "a visitor off the street" — concretely, whose code counts as family here?

Code written inside the declaring class itself, and code inside any class that extends it (however many levels deep) — anything else, no matter how closely related it seems otherwise, counts as an outside visitor.

You have a `Vehicle` base class with an internal `speed` field that only `Vehicle` and its subclasses should touch. Which modifier fits, and why not the other two?

protected — private would block Vehicle's own subclasses from using speed at all, while public would let unrelated, external code touch it too; protected is the one modifier that matches exactly "the class and its subclasses, nothing else."

What is constructor-parameter shorthand, and what two things does writing `constructor(private lives: number = 9) {}` do at once?

Adding an access modifier directly to a constructor parameter both declares lives as a property on the class and assigns it from the argument passed in — a single line replaces what would otherwise be a separate field declaration plus an explicit this.lives = lives; in the constructor body.

Compare `constructor(private lives: number = 9) {}` to declaring `private lives: number;` as a field and assigning it with `this.lives = lives;` in the constructor body. What's actually different?

Nothing about access or runtime behavior differs — both end up with the same private lives property, enforced the same way. The shorthand form is purely a more concise way to write the exact same declaration-plus-assignment, saving a redundant field line.

Why doesn't marking every class property `private` reflexively make a class "safer" in every case?

If a property is genuinely meant to be read or set freely from outside — a simple data holder with no rules to protect — making it private just forces callers into unnecessary getter/setter methods for no real benefit, adding friction without preventing any actual misuse.

When is `public` (the default) actually the right, deliberate choice rather than an oversight?

When a property has no invariant that needs protecting and is genuinely meant to be read or set freely from outside the class — forcing it behind private plus getter methods in that case only adds ceremony without protecting anything real.

What mistake does using `protected` risk that using `private` wouldn't?

It lets any subclass — including ones written later, by someone unfamiliar with the original design — reach in and depend on an internal detail that was never meant to be part of the class's contract, since protected deliberately opens that door to every subclass.

A colleague uses `as any` to bypass a `private` field's type error and reads the value successfully at runtime. Does this reveal a flaw in TypeScript's `private`?

No — it's expected and consistent with how the topic describes enforcement: private is a compile-time-only check with no runtime concept behind it, so any code that gets past the type checker (a cast, or plain untyped JavaScript) can still reach the property, since it's an ordinary property underneath.

The content notes JavaScript has its own native `#field` private syntax, calling it "unrelated" to TypeScript's `private` keyword. What's actually different about it?

TypeScript's private is purely a compile-time annotation — the compiled property is completely ordinary at runtime. JavaScript's native #field syntax is enforced by the language itself at runtime: accessing a #field from outside the class isn't just discouraged, it's an actual restriction the engine enforces regardless of TypeScript or any type checking at all.

Untyped, plain JavaScript code calls into your compiled TypeScript class and directly reads a property you'd marked `private`. Does this throw at runtime?

No — since private produces no runtime restriction, the compiled property is just a normal, readable JavaScript property, so plain JavaScript callers (which never went through TypeScript's checking in the first place) can read or write it without any error at all.

Why are access modifiers described as being about "guiding" other code toward intended public methods rather than about airtight runtime security?

Because the restriction only exists at compile time — it stops other TypeScript code from accidentally reaching in during normal, type-checked development, but it can't stop determined or untyped code from bypassing it at runtime, so it's a design/discipline tool rather than a genuine security boundary.

Given `class Cat { constructor(private lives: number = 9) {} loseLife(): number { return --this.lives; } }`, can code outside `Cat` call `cat.loseLife()`? Can it read `cat.lives` directly?

Yes to the first — loseLife has no modifier, so it defaults to public and is callable from anywhere. No to the second — lives is private via the constructor shorthand, so reading cat.lives from outside the class is a compile error even though the method that changes it is fully public.

If you wanted `lives` in the `Cat` example to be readable from outside but never directly writable, would `private` alone achieve that?

No — private blocks both reading and writing from outside equally; there's no separate modifier for "read from anywhere, write only from inside." Achieving that would mean keeping lives private and adding a public getter method (the same pattern getBalance() already shows for BankAccount), rather than changing which modifier is used.

Between the `BankAccount.balance` example and the `Cat.lives` example, what stays the same in terms of enforced access, and what differs?

Both properties are equally private and equally blocked from outside access — that enforcement is identical. What differs is only how the property gets declared and assigned: BankAccount writes a separate field declaration plus an explicit assignment in the constructor body, while Cat uses constructor-parameter shorthand to do both in the parameter list itself.