TypeScript Classes: Who Can Touch Each Member

A TypeScript class is a JavaScript class with typed fields and access rules. public members are reachable anywhere, protected ones inside the class and its subclasses, private ones only inside the class. Those three checks exist only at compile time. A #name field is private in the running JavaScript too, and readonly blocks writes after the constructor.

Class keywords and what they do
KeywordEffectChecked when
publicReachable from anywhere (the default)Compile time
protectedClass and subclasses onlyCompile time
privateDeclaring class onlyCompile time
#fieldDeclaring class only, enforced by JavaScriptCompile time and runtime
readonlyAssign only in the declaration or constructorCompile time
abstractNo instances; subclasses must fill in membersCompile time
implementsChecks the class matches an interfaceCompile time
overrideMarks a method that replaces a base methodCompile time

Updated

Choose a modifier and the matrix shows five ways code might reach that member, with the tsc result for each. The bracket row is the one that surprises people: the compile-time modifiers can be sidestepped with a string key, while a # field cannot.

Member access matrix

class Account {
  public owner = "Ada";
  protected tier = "pro";
  private balance = 100;
  #pin = 1234;
  readonly id: number = 7;
}

class Savings extends Account { /* subclass */ }
const acct = new Account();

No restrictions. Leaving the modifier off means the same thing.

Access to a public member
Used fromCodetsc result
Inside Account's own methodsthis.ownerCompiles
Inside a Savings methodthis.ownerCompiles
Outside, with a dotacct.ownerCompiles
Outside, with a string keyacct["owner"]Compiles
Outside, assigning a new valueacct.owner = "Bo"Compiles

Class patterns in practice

Parameter properties and a # field

The constructor declares two fields in its parameter list. The cache is a # field, so nothing outside the class can read or clear it.

class PriceService {
  #cache = new Map<string, number>();

  constructor(private readonly baseUrl: string, public currency = "USD") {}

  async price(sku: string): Promise<number> {
    const hit = this.#cache.get(sku);
    if (hit !== undefined) return hit;
    const res = await fetch(`${this.baseUrl}/prices/${sku}`);
    const { amount }: { amount: number } = await res.json();
    this.#cache.set(sku, amount);
    return amount;
  }
}

An abstract base with an override

describe is shared code; area is the hook each shape must supply. With noImplicitOverride on, the override keyword is required on area in Circle.

abstract class Shape {
  abstract area(): number;
  describe(): string {
    return `Area: ${this.area().toFixed(2)}`;
  }
}

class Circle extends Shape {
  constructor(public radius: number) { super(); }
  override area(): number {
    return Math.PI * this.radius ** 2;
  }
}

new Circle(2).describe(); // "Area: 12.57"

implements checks, it does not infer

The interface makes sure the class has send with the right shape, but the parameter still needs its own type annotation.

interface Notifier {
  send(message: string): Promise<void>;
}

class ConsoleNotifier implements Notifier {
  async send(message: string): Promise<void> {
    console.log(message);
  }
}

Fields, constructors and parameter properties

A field declaration such as count = 0 or name: string tells the compiler what the instance holds. Under strict mode each field needs a value by the end of the constructor, which catches the classic bug of a field that is undefined on first use.

Parameter properties fold the declare-then-assign dance into one line. They are handy for services that receive their dependencies through the constructor, though some teams avoid them because they produce real assignment code rather than pure type syntax.

Compile-time privacy versus runtime privacy

public, protected and private are erased when TypeScript emits JavaScript. They stop teammates from misusing a member in typed code, but anything running the output, including a console session, can still read the property.

JavaScript private fields with a # prefix are part of the language. The engine refuses access from outside the class body, they do not show up in Object.keys, and subclasses cannot see them either.

Classes and interfaces work together

An interface describes a shape; a class produces objects that have it. A class can implement several interfaces, and any object with the right members satisfies the interface whether or not a class was involved, because TypeScript compares shapes rather than names.

The interfaces guide on this site covers declaring and extending those shapes. This page is about the class side: construction, access and inheritance.

How it works

  1. Pick a modifier: public, protected, private, #private or readonly.
  2. Read the matrix: each row is a place the member is used from, with Compiles or the tsc error.
  3. Look at the class at the top to see the exact declarations being tested.
  4. Copy the class to try further cases in your own editor.

Tips

Frequently asked questions

What is the difference between private and #private?

private is a TypeScript check. It disappears from the output, and obj["secret"] still compiles and reads the value. A #secret field is a JavaScript private field: there is no string key that reaches it, and code outside the class cannot read it at runtime.

What are parameter properties?

A shortcut that declares and assigns a field from a constructor parameter. Writing constructor(private readonly id: string) {} creates a private readonly id field with no body needed. The prefix can be public, protected, private or readonly.

Does implements change the types inside my class?

No. implements only checks that the finished class fits the interface. Method parameters still need their own annotations; leaving one out under strict mode is an implicit any error, even though the interface already names its type.

Why does TypeScript say a property has no initializer?

strictPropertyInitialization, which strict turns on, wants every declared field set in the declaration or constructor. Give it a value, make it optional, or add ! after the name if something else sets it before use.

When should I use an abstract class instead of an interface?

Use an abstract class when subclasses should share real code, such as a method that calls an abstract hook. An interface carries no code and a class can implement several, so it suits describing shapes.

Related: TypeScript interfaces, TypeScript data types, TypeScript vs JavaScript, TypeScript enums and alternatives, React with TypeScript.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap