Classes and Decorators

Access modifiers, interfaces implemented by classes, abstract classes, and writing a class decorator.

Classes with access modifiers

TypeScript extends JavaScript classes with access modifiers, checked at compile time (they're erased from the compiled JavaScript output, which has no true private enforcement outside of the newer #field syntax):

Typescript
class BankAccount {
  private balance: number;
  public readonly owner: string;
  protected accountType: string = "checking";

  constructor(owner: string, initialBalance: number) {
    this.owner = owner;
    this.balance = initialBalance;
  }

  public deposit(amount: number): void {
    if (amount <= 0) throw new Error("Amount must be positive");
    this.balance += amount;
  }

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

const account = new BankAccount("Ada", 100);
account.deposit(50);
console.log(account.getBalance());   // 150
// account.balance;                  // Error: Property 'balance' is private
Modifier Visible from
public (default) Anywhere
private Only inside this class
protected This class and its subclasses
readonly Anywhere (like public), but can only be assigned once, at construction

A shorthand — parameter properties — declares and assigns a constructor parameter as a class field in one step:

Typescript
class Point {
  constructor(
    public readonly x: number,
    public readonly y: number,
  ) {}
}

const p = new Point(3, 4);
console.log(p.x, p.y);   // 3 4

Interfaces implemented by classes

A class declares it fulfills a contract with implements — the compiler then verifies every required member is actually present:

Typescript
interface Payable {
  calculatePay(): number;
}

class Employee implements Payable {
  constructor(
    private hoursWorked: number,
    private hourlyRate: number,
  ) {}

  calculatePay(): number {
    return this.hoursWorked * this.hourlyRate;
  }
}

function printPay(payable: Payable) {
  console.log(payable.calculatePay());
}

printPay(new Employee(40, 25));   // 1000

A class can implement multiple interfaces (class X implements A, B), and — separately — extend exactly one other class (class X extends Base).

Abstract classes

An abstract class can mix concrete, shared implementation with methods every subclass must implement — and, unlike an interface, it can't be instantiated directly:

Typescript
abstract class Shape {
  abstract area(): number;              // must be implemented by every subclass

  printArea(): void {                    // shared, concrete implementation
    console.log(`Area: ${this.area()}`);
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

// const shape = new Shape();   // Error: Cannot create an instance of an abstract class
const circle = new Circle(5);
circle.printArea();              // Area: 78.53981633974483

Decorators

A decorator is a function applied to a class (or a class member) with @ syntax, letting you add behavior declaratively — logging, validation, dependency-injection registration — without cluttering the class body itself:

Typescript
function logged<T extends new (...args: any[]) => object>(target: T, context: ClassDecoratorContext) {
  return class extends target {
    constructor(...args: any[]) {
      console.log(`Creating instance of ${context.name}`);
      super(...args);
    }
  };
}

@logged
class UserService {
  constructor(public name: string) {}
}

new UserService("Ada");
// Creating instance of UserService

A note on decorator versions. TypeScript 5.0+ implements the modern, TC39 Stage 3 ECMAScript decorators proposal shown above — it works with "target": "ES2022" or later and needs no special compiler flag. You'll still frequently encounter an older, different decorator syntax (used by frameworks like Angular and NestJS) that requires "experimentalDecorators": true and often reflect-metadata — that legacy form predates the official spec and is not directly compatible with the standard one. When starting a new project, prefer the standard (no-flag) decorators unless a specific framework's documentation tells you to enable experimentalDecorators.

Common mistakes

  • Assuming private provides real runtime encapsulation — it's a compile-time-only check, erased in the compiled JavaScript. For genuine runtime privacy, use JavaScript's native #fieldName private field syntax instead.
  • Mixing the legacy experimentalDecorators syntax with the modern standard decorators in the same project — they have different underlying semantics and generally aren't interchangeable.
  • Trying to new an abstract class directly, or forgetting to call super() in a subclass constructor before accessing this.

Interview questions

Q: What's the difference between private in TypeScript and JavaScript's #field syntax? TypeScript's private is a compile-time-only annotation — it disappears entirely from the compiled JavaScript, so code that bypasses the type checker (or plain JS consuming your compiled output) can still access it. JavaScript's native #field is enforced by the runtime itself — attempting to access #field from outside the class is a real runtime error, not just a type error.

Q: When would you use an abstract class instead of an interface in TypeScript? When you want subclasses to share actual concrete method implementations and possibly shared state, not just a method signature contract. An interface only describes shape — it can never carry an implementation; an abstract class can provide default behavior (like printArea() above) while still forcing subclasses to implement the parts that must vary (area()).