TypeScript Fundamentals: Types, Generics and Real-World Patterns · TypeScript Basics: Types, Narrowing and Functions

Classes: Modifiers, Interfaces and Abstract Types

Write classes with typed properties, parameter properties and access modifiers, implement interfaces, use abstract classes and getters, and understand private versus #private.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A small bank: an abstract Account with a protected balance, a SavingsAccount that adds interest, a read-only account number, and a Transferable interface that any class can implement.

The idea

TypeScript classes are JavaScript classes plus types and modifiers.

  • Property declarations: declare the type of every property in the class body.
  • Parameter properties: constructor(public readonly owner: string) declares and assigns a property in one line.
  • Access modifiers: public (default), protected (class and subclasses), private (class only). These are checked at compile time. #field (ECMAScript private) is enforced at runtime too.
  • readonly: assigned only in the declaration or constructor.
  • implements: a class promises to satisfy an interface; the compiler checks it.
  • abstract classes cannot be instantiated and can declare abstract methods that subclasses must implement.
  • Getters and setters (get balance()) expose computed or guarded properties.
  • override (with the noImplicitOverride option) makes the compiler check that a method really overrides one in the parent class.

Prefer composition over deep inheritance: interfaces and small objects are easier to test than class trees.

The code

An abstract Account, a subclass and an interfaceTry it yourself
interface Transferable {
  transferTo(target: Account, amount: number): void;
}

abstract class Account implements Transferable {
  static count = 0;
  readonly number: string;
  #history: string[] = [];             // truly private at runtime
  protected balance = 0;

  constructor(public readonly owner: string, opening = 0) {
    Account.count++;
    this.number = `ACC-${String(Account.count).padStart(3, "0")}`;
    this.deposit(opening);
  }

  abstract monthlyFee(): number;

  get summary(): string {
    return `${this.number} ${this.owner}: ${this.balance.toFixed(2)}`;
  }

  deposit(amount: number): void {
    if (amount < 0) throw new RangeError("amount must be positive");
    this.balance += amount;
    this.#history.push(`+${amount}`);
  }

  withdraw(amount: number): void {
    if (amount > this.balance) throw new Error("insufficient funds");
    this.balance -= amount;
    this.#history.push(`-${amount}`);
  }

  transferTo(target: Account, amount: number): void {
    this.withdraw(amount);
    target.deposit(amount);
  }

  get history(): readonly string[] {
    return this.#history;
  }
}

class CheckingAccount extends Account {
  monthlyFee(): number {
    return 5;
  }
}

class SavingsAccount extends Account {
  constructor(owner: string, opening: number, private rate: number) {
    super(owner, opening);
  }
  monthlyFee(): number {
    return 0;
  }
  addInterest(): void {
    this.deposit(this.balance * this.rate);   // protected: accessible in a subclass
  }
}

const checking = new CheckingAccount("Ada", 100);
const savings = new SavingsAccount("Ada", 1000, 0.02);

checking.transferTo(savings, 40);
savings.addInterest();

console.log(checking.summary);
console.log(savings.summary);
console.log("history:", savings.history.join(" "));
console.log("fees:", checking.monthlyFee(), savings.monthlyFee(), "accounts:", Account.count);

// @ts-expect-error: abstract classes cannot be instantiated
new Account("Nobody");
// @ts-expect-error: balance is protected
console.log(checking.balance);

Output

ACC-001 Ada: 60.00
ACC-002 Ada: 1060.80
history: +1000 +40 +20.8
fees: 5 0 accounts: 2
60

The last line prints 60 even though balance is protected: @ts-expect-error only silences the compiler, and the access modifiers are gone from the compiled JavaScript. That is exactly why #history exists, and why new Account("Nobody") would also run if the compiler didn't stop you.

Common mistakes

  • Assuming private is secure. It only exists at compile time; use #field when it must be hidden at runtime.
  • Forgetting super() first in a subclass constructor that uses parameter properties or this.
  • Deep inheritance trees. More than two levels usually means you want interfaces and composition.
  • Returning a mutable internal array from a getter. Return readonly string[] or a copy.
  • Declaring properties without initializers under strict. Initialise them, mark them optional, or assign in the constructor.

Key points

  • Parameter properties (constructor(public name: string)) remove boilerplate.
  • public, protected and private are compile-time checks; #field is private at runtime as well.
  • implements checks a class against an interface; abstract classes define partial implementations.
  • Getters expose computed values; return readonly types to protect internal state.

Further reading: The Handbook: Classes.

Practice questions

What is the difference between `private` and `#private` in TypeScript classes?

Answer

private is only enforced by the compiler and disappears in the JavaScript output, so untyped code can still read the field. #private is JavaScript's own private field syntax, enforced at runtime.

What does `constructor(public readonly owner: string)` do?

Answer

It is a parameter property: it declares a public, read-only property called owner and assigns the constructor argument to it, without writing the field and assignment separately.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?