Skip to content

Classes, Deep

TypeScript has two ways to make a member private, and they are not the same.

class Account {
private balance = 0; // compile-time only — erased
#pin = "1234"; // truly private — enforced at runtime
check() {
return this.balance + Number(this.#pin);
}
}
const a = new Account();
// a.balance; // ❌ compile error, but...
// (a as any).balance; // ✅ reachable at runtime — private was erased
// a.#pin; // ❌ SyntaxError even at runtime — hard private
  • private (and protected) are compile-time access checks. They are erased on emit, so a determined caller can reach the field at runtime via any or bracket access. They document intent and catch honest mistakes.
  • #name fields are ECMAScript private fields — genuinely inaccessible outside the class, enforced by the JavaScript engine itself. Reach for # when privacy must hold at runtime.

protected sits between: accessible in the class and its subclasses, but not from outside.

readonly allows assignment only in the declaration or constructor, then locks the field. Like private, it is a compile-time guarantee — erased at runtime.

class Point {
readonly x: number;
constructor(x: number) {
this.x = x; // ✅ allowed in the constructor
}
move() {
// this.x = 5; // ❌ Cannot assign to 'x' because it is read-only.
}
}

Parameter properties: the constructor shorthand

Section titled “Parameter properties: the constructor shorthand”

Declaring a field and assigning it from a constructor parameter is so common that TypeScript has a shorthand: put an access modifier on the constructor parameter and it becomes a field automatically.

class User {
constructor(
public readonly id: string,
private name: string,
) {}
// No separate field declarations, no `this.id = id` — the modifiers do it.
}

This is equivalent to declaring id and name as fields and assigning them in the body. It is purely a convenience, but a widely used one.

static members belong to the class itself, not to instances. They exist at runtime as properties on the constructor function.

class Circle {
static readonly PI = 3.14159;
static area(r: number) { return Circle.PI * r * r; }
}
Circle.area(2); // called on the class, not an instance

An abstract class cannot be instantiated directly; it exists to be extended. abstract methods declare a signature that subclasses must implement. The abstract markers are compile-time only.

abstract class Shape {
abstract area(): number; // no body — subclasses must provide one
describe() { return `area is ${this.area()}`; } // concrete method
}
// new Shape(); // ❌ Cannot create an instance of an abstract class.
class Square extends Shape {
constructor(private side: number) { super(); }
area() { return this.side * this.side; } // required
}

A class can implements an interface. This does not copy anything in — it is a compile-time assertion that the class structurally satisfies the interface. Because TypeScript is structural, a class can match an interface without implements; the clause just makes the intent explicit and turns a mismatch into an error at the class definition rather than at the use site.

interface Named { name: string; }
class Product implements Named {
name = "widget"; // if this were missing, the error points HERE
}
What is the key difference between `private balance` and `#pin`?
What does a parameter property (e.g. `constructor(public id: string)`) do?
Where do `static` members live?
What does `implements Named` actually do?