Fields are declared
class Order {
id: string;
items: Door[] = [];
#total = 0; // private at runtime, not only to the compiler
constructor(id: string) {
this.id = id;
}
}
strictPropertyInitialization
A field with no initialiser and no assignment in the constructor is an error. Give it a default, assign it, mark it optional, or — if something else really does set it — use ! and mean it.
Parameter properties
class Order {
constructor(
public readonly id: string,
private items: Door[] = [],
) {}
}
// the same as declaring both fields and assigning them in the body
public, private, protected — and #
# is real privacy, private is a convention the compiler enforces. Use # unless you need a subclass to see the field.
| Enforced by | At runtime | |
|---|---|---|
public | Nothing — it is the default | Visible |
private | The compiler only | Visible: obj["secret"] works |
protected | The compiler only | Visible |
#name | The language | Genuinely inaccessible |
implements
interface Repository<T> {
byId(id: string): T | undefined;
add(item: T): void;
}
class DoorStore implements Repository<Door> {
#items: Door[] = [];
byId(id: string): Door | undefined { return this.#items.find((d) => d.id === id); }
add(item: Door): void { this.#items.push(item); }
}
implements checks, it does not inherit
Nothing is added to the class; the compiler only verifies the shape. And it does not infer parameter types for you — annotate them anyway.
abstract
abstract class Product {
constructor(public readonly id: string) {}
abstract price(): number; // every subclass must write it
describe(): string { return `${this.id}: £${this.price()}`; }
}
class Door extends Product {
constructor(id: string, private base: number) { super(id); }
price(): number { return this.base; }
}
Static, and the class as a value
class Door {
static from(json: unknown): Door { … }
static readonly kind = "door";
}
type DoorInstance = Door; // the instances
type DoorClass = typeof Door; // the class itself, with its statics
function make(ctor: new (id: string) => Door) { return new ctor("a"); }
Getters, setters and this
class Store {
#items: Door[] = [];
get size(): number { return this.#items.length; }
add(item: Door): this { // "this", not "Store" — subclasses chain too
this.#items.push(item);
return this;
}
}
When not to use a class
Classes earn their place where there is genuine state and behaviour together, and a lifetime: a store, a connection, a parser. For the shape of a door, an interface is the whole answer.
- A bag of data with no behaviour: an interface and a plain object are lighter, serialise cleanly and survive a round trip through JSON.
- A single function pretending to be an object with one method.
- Anything you will store in React state or send over the wire — a class instance loses its methods the moment it is serialised.