Class Types & Interfaces

Difficulty: Intermediate

Question

How do classes work with TypeScript's type system? Explain implements, access modifiers, and parameter properties.

Answer

TypeScript enhances JavaScript classes with type annotations, access modifiers, and interface implementation.

Key features: - 'implements' keyword: a class promises to satisfy an interface contract - Access modifiers: public (default), private, protected, and #private (ES native) - Parameter properties: shorthand to declare and assign properties in the constructor - Static members: properties and methods on the class itself, not instances - Structural typing still applies: a class instance satisfies any interface with a matching shape, even without 'implements'

Classes in TypeScript generate both a runtime value (the constructor function) and a type (the instance shape).

Code examples

Implementing Interfaces

interface Serializable {
  serialize(): string;
  deserialize(data: string): void;
}

interface Loggable {
  log(message: string): void;
}

// A class can implement multiple interfaces
class User implements Serializable, Loggable {
  constructor(
    public id: number,
    public name: string,
    public email: string
  ) {}

  serialize(): string {
    return JSON.stringify({ id: this.id, name: this.name, email: this.email });
  }

  deserialize(data: string): void {
    const parsed = JSON.parse(data);
    this.id = parsed.id;
    this.name = parsed.name;
    this.email = parsed.email;
  }

  log(message: string): void {
    console.log(`[User ${this.id}] ${message}`);
  }
}

// Structural typing - no implements needed
const obj = { serialize: () => '{}', deserialize: () => {}, log: () => {} };
const s: Serializable = obj; // OK - has the right shape

implements ensures a class fulfills an interface contract at compile time. However, structural typing means any object with the right shape is compatible.

Access Modifiers & Parameter Properties

class BankAccount {
  // Parameter properties: declare + assign in constructor
  constructor(
    public readonly accountNumber: string,  // public, readonly
    private balance: number,                // only accessible inside class
    protected currency: string = 'USD'      // accessible in subclasses
  ) {}

  // Public method (default)
  getBalance(): string {
    return `${this.currency} ${this.balance.toFixed(2)}`;
  }

  // Private method
  private validateAmount(amount: number): void {
    if (amount <= 0) throw new Error('Amount must be positive');
  }

  deposit(amount: number): void {
    this.validateAmount(amount);
    this.balance += amount;
  }

  withdraw(amount: number): void {
    this.validateAmount(amount);
    if (amount > this.balance) throw new Error('Insufficient funds');
    this.balance -= amount;
  }
}

const account = new BankAccount('ACC-001', 1000);
account.getBalance();       // OK: 'USD 1000.00'
account.accountNumber;      // OK: readonly public
// account.balance;          // Error: private
// account.currency;         // Error: protected
// account.accountNumber = 'X'; // Error: readonly

Parameter properties (public/private/protected/readonly in constructor params) are a shorthand that declares and initializes in one place, reducing boilerplate.

Static Members & Class as Type

class MathUtils {
  // Static property
  static readonly PI = 3.14159;

  // Static method
  static circleArea(radius: number): number {
    return MathUtils.PI * radius  2;
  }

  // Static factory method
  static fromDegrees(degrees: number): number {
    return degrees * (MathUtils.PI / 180);
  }
}

MathUtils.PI;                 // 3.14159
MathUtils.circleArea(5);      // 78.5...
// const m = new MathUtils();  // Works but pointless

// A class creates both a value and a type
class Point {
  constructor(public x: number, public y: number) {}
}

// 'Point' as a type = instance shape
const p: Point = new Point(1, 2);
const p2: Point = { x: 3, y: 4 }; // OK - structural typing

// 'typeof Point' = the class constructor itself
function createInstance(ctor: typeof Point, x: number, y: number): Point {
  return new ctor(x, y);
}

// Generic constructor type
interface Constructor<T> {
  new (...args: any[]): T;
}
function create<T>(ctor: Constructor<T>, ...args: any[]): T {
  return new ctor(...args);
}

A class name used as a type refers to the instance shape. Use 'typeof ClassName' to refer to the constructor. This distinction is important for factory patterns.

Key points

Concepts covered

Classes, implements, Access Modifiers, Parameter Properties, Static Members