Classes and OOP in TypeScript
IntermediateEverything Java gives you is here — access modifiers, abstract classes, interfaces — but the surrounding culture uses far fewer classes, and knowing when not to reach for one matters.
Overview
TypeScript classes will feel immediately familiar: private, protected, readonly, abstract, implements, all present. The adjustment is cultural. Where a Java service layer is a graph of injected classes, the equivalent TypeScript is usually a module of exported functions closing over a client. Classes earn their place when there is genuine instance state with invariants, when you are modelling a domain entity with behaviour, or when a library expects one. A class whose only job is to group three stateless methods is a namespace with extra ceremony.
The Familiar Parts
Modifiers, parameter properties, abstract classes, implements.
abstract class Repository<T> {
protected constructor(protected readonly db: Db) {}
abstract findById(id: string): Promise<T | null>
async requireById(id: string): Promise<T> {
const found = await this.findById(id)
if (!found) throw new NotFoundError(id)
return found
}
}
class ProblemRepo extends Repository<Problem> implements Searchable {
// parameter properties — declares and assigns in one line
constructor(db: Db, private readonly cache: Cache) { super(db) }
async findById(id: string) { ... }
#secret = 'value' // true runtime privacy (JS private field)
private soft = 'value' // compile-time only — visible at runtime
}
// static, getters, and index signatures all work as expected
class Config {
static readonly VERSION = '2.0'
get isProd() { return process.env.NODE_ENV === 'production' }
}private vs #
One is a compiler rule, the other is a runtime guarantee. The difference shows up in tests and in JSON.
class A {
private soft = 1
#hard = 2
}
const a = new A()
a.soft // compile error
(a as any).soft // 1 — it is there at runtime
a['soft'] // 1
a.#hard // SyntaxError — genuinely inaccessible
JSON.stringify(a) // {"soft":1} — # fields are not serialised
// Practical consequences:
// - 'private' is fine for intent; tests can still reach in
// - '#' is real encapsulation, and breaks structural typing
// (two classes with # fields are never compatible)
// - '#' fields disappear from JSON, spreads and Object.keysWhen a Function Is Better
The same logic, without the ceremony. This is the idiomatic default in TypeScript codebases.
// Class version — three stateless methods and a dependency
class ScoreService {
constructor(private db: Db) {}
async readiness(userId: string) { ... }
async weakTopics(userId: string) { ... }
}
const svc = new ScoreService(db)
await svc.readiness(id)
// Module version — same dependencies, less machinery,
// tree-shakes, and each function is independently testable
export async function readiness(db: Db, userId: string) { ... }
export async function weakTopics(db: Db, userId: string) { ... }
// Or close over the dependency once
export function createScoreService(db: Db) {
return {
readiness: (userId: string) => ...,
weakTopics: (userId: string) => ...,
}
}
export type ScoreService = ReturnType<typeof createScoreService>
// Reach for a class when there is real instance state with invariants
// to protect, or a framework that requires one.Key Points to Remember
- 1TypeScript classes support private, protected, readonly, abstract and implements, much as Java does
- 2Parameter properties declare and assign a field in the constructor signature
- 3private is erased at compile time and reachable at runtime; # is genuine encapsulation and is not serialised
- 4A # private field breaks structural compatibility — two classes with one are never interchangeable
- 5Prefer a module of functions or a factory closure over a class that only groups stateless methods
Interview Questions
Sign in to ask AriaWhat is the difference between a private field and a # field?
What are parameter properties in a TypeScript constructor?
When would you choose a class over a module of exported functions?
Ask Aria about Classes and OOP in TypeScript
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.