Typescript · Classes & OOP

Classes & OOP Interview Questions
With Answers & Code Examples

5 carefully curated Classes & OOP interview questions with working code examples and real interview gotchas.

Practice Interactively →← All Categories
5 questions1 beginner3 core1 advanced
Q1Beginner

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

💡 Hint: public = accessible everywhere; private = class only; protected = class + subclasses; readonly = no reassignment after init

TypeScript adds access modifiers to class members to enforce encapsulation (compile-time only — no runtime enforcement).

class BankAccount {
  readonly id: string;          // set once in constructor, never changed
  public owner: string;         // accessible everywhere (default)
  protected balance: number;    // accessible in this class and subclasses
  private #internalRef: string; // true private (JS private field — runtime enforced)

  private log(msg: string) {    // TS private — compile-time only
    console.log(msg);
  }

  constructor(owner: string) {
    this.id = crypto.randomUUID();
    this.owner = owner;
    this.balance = 0;
    this.#internalRef = 'ref';
  }

  deposit(amount: number) {
    this.balance += amount;  // ✅ accessible from own class
    this.log('deposited');   // ✅ private method ok inside class
  }
}

class SavingsAccount extends BankAccount {
  interestRate = 0.05;

  addInterest() {
    this.balance *= (1 + this.interestRate); // ✅ protected accessible in subclass
  }
}

const acct = new BankAccount('Alice');
acct.owner;       // ✅ public
acct.balance;     // ❌ protected — only inside class hierarchy
acct.log('');     // ❌ private

Constructor shorthand — declare and initialize in one line:

class Point {
  constructor(
    public readonly x: number,
    public readonly y: number
  ) {}
  // Equivalent to: this.x = x; this.y = y; declared as readonly public
}
💡 TypeScript's private is erased at runtime. Use JavaScript's native #privateField syntax for true runtime privacy. readonly can be bypassed at runtime but is a strong signal to consumers.
Practice this question →
Q2Core

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

💡 Hint: Cannot be instantiated directly — defines a contract for subclasses; can have concrete methods unlike interfaces

Abstract classes sit between interfaces (pure contracts) and concrete classes (fully implemented). They can have abstract methods (must be implemented by subclasses) and concrete methods (shared implementation).

abstract class Animal {
  abstract sound(): string;     // must be implemented by subclass
  abstract name: string;        // abstract property

  // Concrete method — shared by all subclasses
  describe(): string {
    return `I am a ${this.name} and I say ${this.sound()}`;
  }
}

// new Animal() — ❌ Error: cannot instantiate abstract class

class Dog extends Animal {
  name = 'Dog'; // ✅ implements abstract property

  sound(): string {  // ✅ implements abstract method
    return 'woof';
  }
}

class Cat extends Animal {
  name = 'Cat';
  sound() { return 'meow'; }
}

const dog = new Dog();
console.log(dog.describe()); // "I am a Dog and I say woof"

Abstract class vs interface:

  • Interface — pure contract, no implementation, multiple implementable
  • Abstract class — partial implementation, single inheritance, can have constructors and state
// Abstract class with shared constructor logic
abstract class BaseRepository<T> {
  constructor(protected tableName: string) {}

  abstract findById(id: number): Promise<T | null>;
  abstract save(entity: T): Promise<T>;

  // Shared utility
  protected buildQuery(where: Partial<T>): string {
    // generic SQL builder
    return `SELECT * FROM ${this.tableName}`;
  }
}
💡 Use abstract classes when related classes share implementation logic. Use interfaces when you just want a shape contract. A class can implement multiple interfaces but only extend one abstract class.
Practice this question →
Q3Core

What is the difference between implements and extends in TypeScript?

💡 Hint: extends = inherit implementation (IS-A); implements = satisfy a contract (CAN-DO); class can implement multiple interfaces

extends — inherits from a parent class or interface. Gets all properties, methods, and constructor logic.

implements — declares that a class satisfies an interface contract, but does NOT inherit implementation.

// Interface contract
interface Serializable {
  serialize(): string;
}

interface Loggable {
  log(): void;
}

// extends — inherits implementation
class Animal {
  constructor(public name: string) {}
  breathe() { return 'breathing'; }
}

class Dog extends Animal {
  // Inherits name, breathe() from Animal
  bark() { return 'woof'; }
}

// implements — satisfy contract without inheriting
class User implements Serializable, Loggable {
  constructor(public name: string, public email: string) {}

  // MUST implement all interface methods
  serialize() { return JSON.stringify({ name: this.name, email: this.email }); }
  log() { console.log(this.name); }
}

// Combining both
class AdminUser extends User implements Comparable<AdminUser> {
  constructor(name: string, email: string, public adminLevel: number) {
    super(name, email); // must call parent constructor
  }

  compareTo(other: AdminUser): number {
    return this.adminLevel - other.adminLevel;
  }
}

Interface implements itself (structural check):

// A class can implement an interface to get compile-time checking
// that it fulfills the contract — even without runtime cost
class Repository implements IRepository<User> {
  // TypeScript errors if any method is missing
}
💡 TypeScript is structurally typed — you don't NEED to write implements if the class already has the right shape. But writing it explicitly documents intent and gives clearer error messages when the contract isn't met.
Practice this question →
Q4Core

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

💡 Hint: Decorators are factory functions applied to classes, methods, or properties — common for metadata, dependency injection, logging

Decorators are functions that wrap or modify classes, methods, properties, or parameters. Enabled with experimentalDecorators: true in tsconfig.

// Class decorator — modify or augment a class
function Singleton<T extends new(...args: any[]) => any>(Ctor: T) {
  let instance: InstanceType<T>;
  return class extends Ctor {
    constructor(...args: any[]) {
      if (instance) return instance;
      super(...args);
      instance = this as any;
    }
  };
}

@Singleton
class Config { /* only one instance ever created */ }

// Method decorator — wrap a method with extra behavior
function log(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${key} with`, args);
    const result = original.apply(this, args);
    console.log(`${key} returned`, result);
    return result;
  };
  return descriptor;
}

class Calculator {
  @log
  add(a: number, b: number): number {
    return a + b;
  }
}

// Property decorator — metadata annotation
function required(target: any, propertyKey: string) {
  // metadata stored here — used by validation frameworks
}

class User {
  @required
  name: string = '';
}
💡 Decorators are widely used in Angular, NestJS, TypeORM, and class-validator. TypeScript 5.0 introduced the new decorator standard (TC39 Stage 3) that differs from the legacy experimental decorators.
Practice this question →
Q5Advanced

What are static members and class fields in TypeScript?

💡 Hint: static members belong to the class itself, not instances; class fields vs constructor assignments have subtle initialization order differences

Static members belong to the class itself, not to any instance. Accessed via the class name.

class Counter {
  static count = 0;  // shared across all instances
  id: number;

  constructor() {
    Counter.count++; // increment class-level counter
    this.id = Counter.count;
  }

  static reset() {
    Counter.count = 0;
  }

  static getInstance() {
    return new Counter();
  }
}

const a = new Counter(); // Counter.count = 1
const b = new Counter(); // Counter.count = 2
console.log(Counter.count); // 2

Counter.reset();
console.log(Counter.count); // 0

Class fields vs constructor assignment:

class Example {
  // Class field — initialized BEFORE constructor body
  x = 10;
  y = this.x * 2; // ✅ x is already 10

  // Method defined as field — each instance gets its own copy
  handleClick = () => {
    console.log(this.x); // 'this' is always correct — no binding needed
  };

  constructor() {
    // Class fields are set BEFORE this runs
    console.log(this.x); // 10
  }
}

// vs prototype method — shared across all instances
class ExampleProto {
  handleClick() {
    // 'this' depends on how it's called — may need .bind()
    console.log(this);
  }
}
💡 Arrow function class fields (like handleClick = () => {}) solve the React event handler binding issue — no need for .bind(this) in the constructor. The trade-off is each instance gets its own function copy rather than sharing a prototype method.
Practice this question →

Other Typescript Interview Topics

Rendering StrategiesCore JSType SystemReact FundamentalsFunctionsMicrofrontendsGenericsAsync JSHooksObjectsMonorepoArrays'this' KeywordUtility TypesError HandlingModern JSBundle OptimizationPerformanceDOM & EventsState ManagementCaching StrategiesComponent PatternsAdvanced TypesAuthenticationReact RouterFormsAdvanced PatternsFrontend SecurityConcurrent ReactServer ComponentsTestingEcosystemNetwork OptimizationCore Web VitalsBrowser APIs

Ready to practice Classes & OOP?

Get AI feedback on your answers, predict code output, and fix real bugs.

Start Free Practice →