Intermediate4 questionsFull Guide

TypeScript Classes & Access Modifiers — Complete Interview Guide

Deep-dive into TypeScript classes — public, private, protected, readonly, abstract, and the new #private fields. Understand parameter properties, class vs interface, static members, and how TypeScript's structural typing interacts with access modifiers.

The Mental Model

TypeScript access modifiers are like the different rooms in a building. Public members are the lobby — anyone can walk in from outside. Protected members are the staff corridor — only employees (the class itself and its subclasses) can access them. Private members are the manager's office — only the person in that exact office (the class itself, no subclasses) can enter. TypeScript's private keyword enforces this at compile time; JavaScript's # syntax (hard private) enforces it at runtime as well, making it a true encapsulation boundary even in plain JavaScript.

The Explanation

Access Modifiers Overview

class BankAccount {
  public  owner: string;       // Accessible from anywhere (default)
  protected balance: number;   // Accessible in this class and subclasses
  private  pin: string;        // Accessible only in this class
  readonly accountId: string;  // Can be set once (in constructor), then immutable

  constructor(owner: string, pin: string) {
    this.owner = owner;
    this.pin = pin;
    this.balance = 0;
    this.accountId = crypto.randomUUID();
  }

  public getBalance(): number {
    return this.balance; // Can access private/protected inside the class
  }

  private validatePin(input: string): boolean {
    return input === this.pin; // Only this class can call validatePin
  }
}

const account = new BankAccount("Alice", "1234");
account.owner;          // OK — public
account.balance;        // Error — protected
account.pin;            // Error — private
account.accountId;      // OK — readonly, but readable
account.accountId = "x"; // Error — readonly

Parameter Properties — Shorthand Constructor

TypeScript's parameter properties declare and initialise class properties directly from constructor parameters — eliminating boilerplate:

// Verbose — the traditional way
class User {
  public name: string;
  private email: string;
  protected role: string;

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

// Concise — parameter properties (access modifier in constructor signature)
class User {
  constructor(
    public  name: string,
    private email: string,
    protected role: string,
    readonly id: number = Math.random()
  ) {}
  // name, email, role, id are automatically declared AND initialised
}

TypeScript private vs JavaScript # (Hard Private)

TypeScript's private is a compile-time-only check — at runtime the property is still publicly accessible on the object. JavaScript's # syntax creates a true runtime private field that cannot be accessed even via reflection:

class TokenService {
  private tsPrivate = "secret";    // TypeScript only — erased at runtime
  #jsPrivate = "also-secret";      // JavaScript private field — runtime enforced

  getTokenTs() { return this.tsPrivate; }
  getTokenJs() { return this.#jsPrivate; }
}

const svc = new TokenService();
(svc as any).tsPrivate; // Works at runtime — TypeScript private is compile-time only!
(svc as any)["#jsPrivate"]; // undefined — hard private, inaccessible even with any
svc.#jsPrivate; // Error at compile time AND runtime
For truly sensitive data (tokens, keys, passwords in memory), prefer # hard private fields. Use TypeScript private for API encapsulation enforced by the type system.

Abstract Classes

Abstract classes define a template — they can contain implemented methods AND abstract methods that subclasses must implement. They cannot be instantiated directly:

abstract class Repository<T> {
  // Concrete method — shared implementation available to all subclasses
  async findOrFail(id: string): Promise<T> {
    const entity = await this.findById(id);
    if (!entity) throw new Error(`Not found: ${id}`);
    return entity;
  }

  // Abstract methods — subclasses MUST implement these
  abstract findById(id: string): Promise<T | null>;
  abstract save(entity: T): Promise<void>;
  abstract delete(id: string): Promise<void>;
}

class UserRepository extends Repository<User> {
  async findById(id: string): Promise<User | null> {
    return db.users.findOne({ id });
  }
  async save(user: User): Promise<void> {
    await db.users.upsert(user);
  }
  async delete(id: string): Promise<void> {
    await db.users.delete({ id });
  }
}

new Repository(); // Error — cannot instantiate abstract class
new UserRepository(); // OK

Static Members

class Config {
  private static instance: Config | null = null;
  private settings: Record<string, string> = {};

  // Singleton pattern — static factory method
  static getInstance(): Config {
    if (!Config.instance) {
      Config.instance = new Config();
    }
    return Config.instance;
  }

  // Static utility method — no instance needed
  static fromEnv(): Config {
    const cfg = new Config();
    cfg.settings = { ...process.env } as Record<string, string>;
    return cfg;
  }
}

const cfg = Config.getInstance(); // Access via class, not instance

TypeScript Structural Typing and Access Modifiers

TypeScript uses structural typing — two types are compatible if their shapes match. But access modifiers affect structural compatibility in a subtle way:

class A { private secret = 1; public value = 2; }
class B { private secret = 1; public value = 2; }

// Despite looking identical, A and B are NOT structurally compatible
// because their private fields are declared in different classes
const a: A = new B(); // Error — A's private 'secret' is not from B

class C extends A {} // OK — C inherits A's private 'secret'
const c: A = new C(); // OK — C is structurally compatible with A

Common Misconceptions

⚠️

Many developers think TypeScript's private keyword prevents runtime access — it only prevents access through the TypeScript type system. At runtime, (obj as any).privateField works fine. Use # for true runtime encapsulation.

⚠️

Many developers think abstract classes and interfaces are interchangeable — abstract classes can contain implementations; interfaces cannot. Use an interface when you only want to define a contract. Use an abstract class when you want to share implementation between subclasses.

⚠️

Many developers think private fields from different classes with the same structure are compatible in TypeScript's structural type system — they are not. Private fields break structural compatibility between unrelated classes, even if the shapes look identical.

⚠️

Many developers think parameter properties are only syntactic sugar with no behavioral difference — they are identical at runtime, but parameter properties must have an access modifier (public, private, protected, or readonly) or TypeScript treats them as plain constructor parameters without property declarations.

⚠️

Many developers think readonly means immutable deeply — readonly only prevents reassignment of the property itself. If the property holds an array or object, its contents are still mutable. Use Readonly<T> or as const for deeper immutability.

⚠️

Many developers think static members are shared between subclasses automatically — each class has its own static properties. Subclasses inherit access to parent static methods but have their own static property slots.

Where You'll See This in Real Code

Singleton services: Config.getInstance() pattern using static instance property is ubiquitous in backend TypeScript code for database connections, configuration managers, and logging services.

Repository pattern: abstract Repository<T> with abstract CRUD methods is a standard architecture pattern in NestJS, TypeORM, and Prisma-based backends — concrete repositories implement the abstract methods per entity.

Event emitter encapsulation: private listeners map in an EventEmitter class prevents external code from directly manipulating the listener registry, while protected methods let subclasses hook into the emit lifecycle.

React class components (legacy): public render(), private handleClick(), protected shouldRenderSidebar() map directly to the mental model of what API surface should be part of the contract vs internal implementation.

NestJS providers: TypeScript classes with @Injectable(), private readonly dependencies injected via constructor parameter properties, and public methods forming the service API — parameter properties save 30–50% of boilerplate in medium-size services.

Sensitive data holders: # hard private fields for token storage in auth utilities, ensuring that even injected runtime code or serialization libraries cannot accidentally expose the field — TypeScript private would not provide this guarantee.

Interview Cheat Sheet

  • public: accessible everywhere (default if no modifier is written)
  • private: compile-time only — accessible only within the declaring class
  • #field: JavaScript hard private — runtime enforced, inaccessible even via any cast
  • protected: accessible in the declaring class and all subclasses
  • readonly: can be set in constructor only, then immutable (top-level, not deep)
  • Parameter properties: add access modifier to constructor param to auto-declare and initialise
  • abstract class: can have implementations + abstract methods; cannot be instantiated directly
  • abstract method: must be implemented by every non-abstract subclass
  • static: belongs to the class itself, not instances; not inherited into subclass slots
  • Private fields break structural compatibility between unrelated classes in TypeScript's type system
💡

How to Answer in an Interview

  • 1.The TypeScript private vs # distinction is a strong signal: 'TypeScript private is compile-time only — (obj as any).field still works at runtime. JavaScript # creates a true runtime boundary. For genuinely sensitive in-memory data, I prefer # over TypeScript private.'
  • 2.Use the abstract class vs interface contrast to show design understanding: 'I reach for an interface when I only want a contract — no shared code. I reach for an abstract class when I want to share implementation logic between concrete subclasses, like a base Repository with findOrFail already implemented.'
  • 3.Parameter properties are a good TypeScript-specific trick to demonstrate: 'TypeScript's parameter properties let you declare and initialise a class member in one place — the constructor signature. This is TypeScript-specific syntax that disappears in the compiled output but saves significant boilerplate in service classes.'
  • 4.The structural typing + private fields interaction is a senior-level point: 'TypeScript uses structural typing, but private fields break it between unrelated classes. Two classes with the same shape but different private field origins are not assignable to each other — this is intentional to prevent accidental coupling.'

Practice Questions

4 questions
#01

What are access modifiers in TypeScript — public, private, protected, readonly?

EasyClasses & OOP PRO💡 public = accessible everywhere; private = class only; protected = class + subclasses; readonly = no reassignment after init
#02

What are abstract classes in TypeScript and when should you use them?

EasyClasses & OOP PRO💡 Cannot be instantiated directly — defines a contract for subclasses; can have concrete methods unlike interfaces
#03

What is the difference between implements and extends in TypeScript?

EasyClasses & OOP PRO💡 extends = inherit implementation (IS-A); implements = satisfy a contract (CAN-DO); class can implement multiple interfaces
#04

How do decorators work in TypeScript, and what are they used for?

EasyClasses & OOP PRO💡 Decorators are factory functions applied to classes, methods, or properties — common for metadata, dependency injection, logging

Related Topics

TypeScript Types vs Interfaces — Complete Interview Guide
Intermediate·8–12 Qs
TypeScript Decorators — Complete Interview Guide
Advanced·4–8 Qs
TypeScript Generics — Complete Interview Guide
Intermediate·8–12 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz