Classes, access modifiers and abstract
public, private, protected and readonly, how TypeScript privacy differs from a JavaScript
After this lesson you can
- Use parameter properties and the four member modifiers
- Explain the runtime difference between private and a
- Write an abstract class whose concrete method calls abstract steps
- Say when implements is enough and when a base class is worth it
A TypeScript class is a JavaScript class with types and a few extra keywords. The one shorthand worth knowing is the parameter property: a modifier on a constructor parameter declares the field and assigns it.
class Invoice {
constructor(
public readonly id: string, // declared and assigned in one go
private total: number,
) {}
}
The modifiers
public— the default; usable anywhere.protected— usable inside the class and its subclasses.private— usable only inside the class that declares it.readonly— assignable only in the declaration or the constructor.
All four are compile-time checks. They are erased with the rest of the types, so the field is an ordinary property at runtime.
private against #private
JavaScript has its own private fields, written #name. Those are enforced
by the runtime: the property does not exist from the outside at all, so
serialisation, Object.keys and a cast cannot reach it.
class Account { private balance = 0; // TypeScript-only privacy #pin: string; // JavaScript private field: enforced at runtime constructor(readonly owner: string, pin: string) { this.#pin = pin; } deposit(amount: number): void { this.balance += amount; } checkPin(guess: string): boolean { return guess === this.#pin; }} export function run(): unknown[] { const acct = new Account("Nino", "4321"); acct.deposit(50); return [ JSON.stringify(acct), // what a log line or an API response sees (acct as any).balance, // "private" is one cast away (acct as any).pin ?? "no such key", // #pin is not a property at all acct.checkPin("4321"), ];}balance is "private" to the compiler and plainly visible to
JSON.stringify and to anyone who writes as any. #pin is not visible to
either. Use # when the value must not leak (a secret, a token); use
private when the aim is only to keep other code from depending on an
implementation detail.
Abstract classes
An abstract class cannot be instantiated by name, and may declare
abstract members with no body that every concrete subclass must
implement. The useful pattern is a concrete method in the base class that
fixes the order of the steps and calls abstract ones — the subclasses fill
in the steps, never the order:
abstract class Report { constructor(protected readonly title: string) {} // Each subclass says how; the base class owns the order of the steps. protected abstract body(): string; render(): string { return `# ${this.title}\n${this.body()}`; }} class SalesReport extends Report { constructor(private readonly total: number) { super("Sales"); } protected body(): string { return `total: ${this.total}`; }} export function run(): string[] { const out = [new SalesReport(1200).render()]; // abstract is erased too: at runtime Report is an ordinary class. try { const bare = new (Report as any)("Bare"); out.push(bare.render()); } catch (e) { out.push(String(e)); } return out;}abstract is erased as well. With a cast, Report itself can be
constructed at runtime, and the missing step fails only when it is called.
implements or extends
class Cache implements Store only checks that the class has the shape of
Store; it inherits nothing. extends inherits code. Prefer an interface
plus implements for a contract, and an abstract base class only when
there is shared behaviour worth inheriting.
Common mistakes
- Treating
privateas security. It hides nothing at runtime; the example's balance is in the JSON. Use#fieldfor data that must not escape. - Calling an overridable method from a constructor. The base
constructor runs before the subclass's own fields are initialised, so a
subclass method called from there sees them
undefined(the example after this list). - Using
abstractwhere an interface is enough. If the base class has no shared code, an interface keeps the hierarchy flat. - Expecting
readonlyfields to be frozen. Like every modifier, it is checked by the compiler only.
The constructor-order mistake, run:
abstract class Widget { readonly label: string; constructor() { // Runs before Button's own fields have been initialised. this.label = this.describe(); } protected abstract describe(): string;} class Button extends Widget { private readonly text = "Save"; protected describe(): string { return `button: ${this.text}`; }} export function run(): string[] { const b = new Button(); return [b.label, (b as any).describe()];}What interviewers ask
- What is the difference between
privateand#private?privateis a compile-time check on an ordinary property;#privateis a JavaScript private field that the runtime itself keeps hidden. - Abstract class or interface? An interface is a pure contract, erased completely; an abstract class can carry shared implementation and constructor logic, and exists at runtime.
- What is
protectedfor? Members that subclasses need but outside callers should not touch — typically the steps of a template method.
Exercise · Pro
Two exporters that share one skeleton
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade