TypeScript

Classes in TypeScript

JavaScript already has classes — TypeScript adds access modifiers, a genuine abstract-class mechanism, and a shorthand for the single most repetitive part of writing one. None of it changes what a class fundamentally is, all of it is compile-time-only.

Intermediate

4 min read

The access modifiers JavaScript classes don't have on their own

class BankAccount {
  public owner: string;      // accessible from anywhere (the default — rarely written explicitly)
  private balance: number;   // accessible only inside this class
  protected accountType: string; // accessible in this class AND subclasses
 
  constructor(owner: string, balance: number) {
    this.owner = owner;
    this.balance = balance;
    this.accountType = "checking";
  }
 
  deposit(amount: number) {
    this.balance += amount; // fine — inside the class
  }
}
 
const acc = new BankAccount("Ada", 100);
acc.balance; // Error: Property 'balance' is private and only accessible within class 'BankAccount'

public, private, and protected are compile-time-only access control — like every other TypeScript-specific construct, they're erased entirely during compilation and enforce nothing at runtime (real, runtime-enforced privacy needs JavaScript's own native #balance syntax instead, which TypeScript also supports and which genuinely can't be accessed from outside the class, even after compilation). private restricts access to inside the declaring class only; protected extends that same access to subclasses too; public is the default and rarely needs to be written explicitly.

Constructor parameter properties: the shorthand for the most repetitive pattern

// The verbose, explicit version
class Point {
  x: number;
  y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}
 
// The parameter-property shorthand — identical result, far less repetition
class PointShort {
  constructor(public x: number, public y: number) {}
}

Writing public x: number directly inside the constructor's parameter list is TypeScript-specific shorthand: it declares the field, types it, and assigns this.x = x automatically, all from one line. This is one of the most commonly used TypeScript-only conveniences in real code — the field-declaration-plus-constructor-assignment pattern is repetitive enough, and common enough, that this shorthand meaningfully reduces boilerplate on nearly every class with simple constructor-assigned fields.

implements: an explicit promise that a class satisfies an interface

interface Shape {
  area(): number;
}
 
class Circle implements Shape {
  constructor(private radius: number) {}
  area(): number {
    return Math.PI * this.radius ** 2;
  }
}
 
class Broken implements Shape {
  // Error: Class 'Broken' incorrectly implements interface 'Shape' —
  // Property 'area' is missing
}

As the structural-typing lesson covered, a class doesn't technically need implements for its instances to satisfy an interface — structural compatibility alone would make Circle instances satisfy Shape even without it. implements is still genuinely valuable: it turns "does this class actually match the interface" into an immediate compile error at the class declaration itself, the moment a required method is missing or mismatched, rather than a confusing error much later, wherever a Broken instance eventually gets used somewhere expecting a Shape.

Abstract classes: a real compile-time enforcement mechanism, unlike a plain base class

abstract class Shape {
  abstract area(): number; // no implementation — subclasses MUST provide one
 
  describe(): string {
    return `This shape has an area of ${this.area()}`; // can call an abstract method
  }
}
 
class Circle extends Shape {
  constructor(private radius: number) { super(); }
  area(): number { return Math.PI * this.radius ** 2; }
}
 
new Shape(); // Error: Cannot create an instance of an abstract class
new Circle(5).describe(); // fine — Circle implements area()

abstract class can't be instantiated directly at all (new Shape() is a compile error), and any method marked abstract has no body in the base class — every concrete subclass is required to implement it, checked at compile time, the same enforcement the OOP domain's ABC lesson covers for Python's abc module. describe() above can call this.area() even though Shape itself never implements it, because by the time describe() actually runs on a real instance, that instance is guaranteed (by the abstract requirement) to be some concrete subclass that does implement it.

Static members: belonging to the class itself, not any instance

class Counter {
  private static count = 0;
 
  constructor() {
    Counter.count++;
  }
 
  static getCount(): number {
    return Counter.count;
  }
}
 
new Counter();
new Counter();
Counter.getCount(); // 2 — shared across every instance, not per-instance

static members belong to the class itself, one shared copy, rather than existing separately on every instance — Counter.count is a single counter incremented by every new Counter() call, accessed via Counter.getCount() rather than through any particular instance. This is the same idea the Python domain's static/class-method lesson covers, expressed with TypeScript's own static keyword instead of Python's @classmethod/@staticmethod decorators.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. Do TypeScript's `public`/`private`/`protected` access modifiers enforce anything at runtime, in the compiled JavaScript?

2. What does the constructor parameter-property shorthand `constructor(public x: number) {}` actually do?

3. Since structural typing means a class doesn't technically need `implements SomeInterface` to satisfy it, why write `implements` anyway?

4. Why can `new Shape()` never succeed if Shape is declared `abstract class Shape { abstract area(): number; }`?