04 - Classes

Basic Class

class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet(): string {
    return `Hi, I'm ${this.name}`;
  }
}

const person = new Person("Alice", 30);
person.greet();  // "Hi, I'm Alice"

Access Modifiers

Control who can access properties:

class BankAccount {
  public owner: string;        // accessible everywhere (default)
  private balance: number;     // only inside this class
  protected accountNumber: string;  // inside this class + subclasses

  constructor(owner: string, balance: number) {
    this.owner = owner;
    this.balance = balance;
    this.accountNumber = Math.random().toString();
  }

  deposit(amount: number): void {
    this.balance += amount;  // ✅ private accessible inside the class
  }
}

const account = new BankAccount("Alice", 100);
account.owner;    // ✅ public
account.balance;  // ❌ error — private

Shorthand Constructor

TypeScript shortcut — declare and assign properties in one step:

// Instead of this:
class User {
  name: string;
  email: string;
  id: number;

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

// Write this:
class User {
  constructor(
    public name: string,
    private email: string,
    readonly id: number  // readonly = can't be changed after creation
  ) {}
}

Inheritance

A class can extend another class and inherit its properties/methods:

class Animal {
  constructor(public name: string) {}
  
  move(): void {
    console.log(`${this.name} is moving`);
  }
}

class Dog extends Animal {
  bark(): void {
    console.log("Woof!");
  }
}

const dog = new Dog("Buddy");
dog.move();  // "Buddy is moving" — inherited from Animal
dog.bark();  // "Woof!" — own method

Abstract Classes

Can't be instantiated directly — only used as a base for other classes. Forces subclasses to implement certain methods:

abstract class Shape {
  abstract area(): number;  // subclasses MUST implement this

  describe(): string {      // shared method — subclasses inherit this
    return `This shape has area ${this.area()}`;
  }
}

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

  area(): number {  // required — abstract method
    return Math.PI * this.radius ** 2;
  }
}

// const shape = new Shape();     // ❌ error — can't instantiate abstract class
const circle = new Circle(5);     // ✅
circle.area();                    // 78.54
circle.describe();                // "This shape has area 78.54..."

Implementing Interfaces

A class can implement an interface — guaranteeing it has certain properties/methods:

interface Printable {
  print(): string;
}

interface Loggable {
  log(): void;
}

class Report implements Printable, Loggable {  // must implement both
  constructor(private title: string) {}

  print(): string {
    return `Report: ${this.title}`;
  }

  log(): void {
    console.log(this.print());
  }
}

Interface vs Class — When to Use Which

An interface only exists at compile time — it defines a shape but produces zero JavaScript. A class exists at runtime — it's real code with constructors and methods.

// Interface — erased after compilation, no JavaScript output
interface User {
  name: string;
  age: number;
}
const user: User = { name: 'Alice', age: 30 }; // just a plain object

// Class — real JavaScript, stays in the bundle
class UserAccount {
  constructor(public name: string, public age: number) {}
  greet() { return `Hi, I'm ${this.name}`; }
}
const account = new UserAccount('Alice', 30);
account.greet();

Interfaces can define method signatures, but never implementations:

interface UserService {
  getUser(id: number): Promise<User>;
  deleteUser(id: number): Promise<void>;
}

// You don't need a class to satisfy it — any object with the right shape works:
const service: UserService = {
  async getUser(id) { return fetch(`/api/users/${id}`).then(r => r.json()); },
  async deleteUser(id) { await fetch(`/api/users/${id}`, { method: 'DELETE' }); }
};

This is called structural typing — TypeScript doesn't care if you explicitly implements an interface. If the shape matches, it's valid.

Interface Class
Runtime existence ❌ erased ✅ real JS
Method logic ❌ signatures only ✅ full implementations
instanceof check
Bundle size impact None Adds code
Constructors
implements required No (structural typing) N/A

Rule of thumb: Use interfaces by default for typing data shapes, API responses, and function parameters. Use classes only when you need runtime behavior — constructors, methods with logic, or instanceof checks.

Key Takeaways

  • public (default), private, protected control access
  • Shorthand constructor: put modifiers in constructor params
  • readonly = can't be changed after creation
  • extends = inherit from a class
  • abstract = base class that can't be instantiated, forces subclasses to implement methods
  • implements = class must follow an interface's contract
  • Interface = compile-time only, defines shape, no runtime cost — use by default
  • Class = runtime code, use when you need constructors, methods, or instanceof
  • TypeScript uses structural typing — if the shape matches, it satisfies the interface (no implements needed)