Home/Learn/JavaScript & TypeScript/Classes and OOP in TypeScript

Classes and OOP in TypeScript

Intermediate
TypeScript

Everything 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.

private, abstract, implements, parameter properties
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.

Compile-time privacy vs runtime privacy
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.keys

When a Function Is Better

The same logic, without the ceremony. This is the idiomatic default in TypeScript codebases.

A module of functions, or a factory
// 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 Aria
1

What is the difference between a private field and a # field?

Medium
2

What are parameter properties in a TypeScript constructor?

Easy
3

When would you choose a class over a module of exported functions?

Medium

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.

Loading discussion…