Notifications

No notifications

/Phase 3

Classes in TS

TypeScript classes are JavaScript classes plus a type system on top: public / private / protected visibility, readonly fields, abstract classes, parameter properties, and the difference between implements and extends. You'll use them whenever you build a service, a value object, or a React class component (rare these days).

On this page

Detailed Theory

A typed class

class User {
  name: string;
  age: number;

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

greet(): string { return Hi ${this.name}; } }

const u = new User("Ada", 36); console.log(u.greet());

Parameter properties — the shorthand

Add a visibility modifier to a constructor parameter and TS auto-creates the field + assigns it.

class User {
  constructor(public name: string, public age: number) {}
  greet() { return Hi ${this.name}; }
}

Five lines instead of nine. Use it everywhere.

Visibility modifiers

ModifierVisible from
public (default)Anywhere
protectedThe class and its subclasses
privateOnly the class itself
#field (JS)Truly private at runtime

class Account {
  protected balance = 0;
  private secret = "hunter2";

deposit(n: number) { this.balance += n; } }

class Savings extends Account { bonus() { this.balance *= 1.05; } // ✅ protected // leak() { return this.secret; } // ❌ private }

private is enforced by TypeScript only — JS code can still poke the field. For real runtime privacy use #field:

class Wallet {
  #pin: string;
  constructor(pin: string) { this.#pin = pin; }
  // No way to read #pin from outside, even at runtime
}

readonly fields

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

const p = new Point(1, 2); // p.x = 5; // ❌

Static members

Belong to the class, not instances.

class Logger {
  static instances = 0;
  constructor() { Logger.instances++; }
}
new Logger(); new Logger();
console.log(Logger.instances); // 2

Inheritance — extends + super

class Animal {
  constructor(public name: string) {}
  speak() { return ${this.name} makes a sound; }
}

class Dog extends Animal { constructor(name: string, public breed: string) { super(name); } speak() { return ${this.name} barks!; } // override }

const d = new Dog("Rex", "Lab"); console.log(d.speak()); // "Rex barks!"

Use super.method() to call the parent's version when overriding:

class Loud extends Dog {
  speak() { return super.speak().toUpperCase(); }
}

Abstract classes

abstract classes can't be instantiated — they exist to be extended.

abstract class Shape {
  abstract area(): number;            // subclass MUST implement
  describe() { return Area: ${this.area()}; }
}

class Circle extends Shape { constructor(public r: number) { super(); } area() { return Math.PI * this.r ** 2; } }

// new Shape(); // ❌ new Circle(5); // ✅

Use abstract classes when you want shared implementation + a contract. Use interfaces when you only want the contract.

implements — promise to satisfy a shape

interface Repository<T> {
  find(id: number): Promise<T | null>;
  save(item: T): Promise<void>;
}

class UserRepo implements Repository<User> { async find(id: number) { return null; } async save(u: User) {} }

You can implement *multiple* interfaces; you can only extends one class.

Getters & setters

class Temperature {
  #celsius = 0;

get fahrenheit() { return this.#celsius * 9 / 5 + 32; } set fahrenheit(f: number) { this.#celsius = (f - 32) * 5 / 9; } }

const t = new Temperature(); t.fahrenheit = 212; console.log(t.fahrenheit); // 212

Generic classes (recap)

class Stack<T> {
  private items: T[] = [];
  push(x: T) { this.items.push(x); }
  pop(): T | undefined { return this.items.pop(); }
}

When to use a class vs a plain object

Use a classUse a plain object
Identity matters (instanceof)Pure data
You need methods that operate on shared stateMost React props / API responses
You want inheritance or a contractFunctional code
You're building a service / repositoryStateless utilities

In modern frontend code, plain objects + standalone functions usually win — classes shine in services, ORMs, parsers, and games.

A common pitfall — losing this

class Counter {
  count = 0;
  inc() { this.count++; }
}
const c = new Counter();
const fn = c.inc;
// fn(); // ❌ Cannot read 'count' of undefined

Fix by binding (fn = c.inc.bind(c)) or using an arrow-function field:

class Counter {
  count = 0;
  inc = () => { this.count++; };  // bound to instance forever
}