Skip to content

Access Modifiers and Readonly

In Intermediate JavaScript you used the # syntax for private class fields. TypeScript offers its own access modifiers — public, private, and protected — that integrate with the type system and provide additional patterns JavaScript’s # does not support.

public — visible everywhere. This is the default; writing public is optional but occasionally used for clarity.

private — visible only within the class body. TypeScript prevents access from outside the class at compile time.

protected — visible within the class body and any subclass. Not accessible from outside the class hierarchy.

class QuizEngine {
private questions: TriviaQuestion[];
private _index: number = 0;
private _score: number = 0;
constructor(questions: TriviaQuestion[]) {
this.questions = questions;
}
get total(): number { return this.questions.length; }
get index(): number { return this._index; }
get currentScore(): number { return this._score; }
}
const engine = new QuizEngine(questions);
engine._index = 5; // Error: Property '_index' is private
engine.questions; // Error: Property 'questions' is private
engine.total; // ✓ — getter is public

The questions, _index, and _score fields cannot be read or written from outside the class. Only the public getters expose them.

TypeScript private vs JavaScript private (#)

Section titled “TypeScript private vs JavaScript private (#)”

TypeScript’s private modifier and JavaScript’s # field syntax both restrict access, but they work differently:

FeatureTypeScript privateJavaScript #
Enforced atCompile time onlyRuntime + compile time
Accessible at runtime?Yes — via (obj as any).propNo — truly private
Appears in compiled JS?Yes, as a regular propertyYes, as a #field

AceIt uses JavaScript’s # syntax (#id, #createdAt) in Expense from Intermediate JavaScript because true runtime privacy was needed. For QuizEngine in TypeScript, either approach works — the TypeScript private modifier is sufficient because the compiled JS is the only consumer of the class.

readonly prevents a property from being reassigned after the constructor runs:

class QuizEngine {
private readonly questions: TriviaQuestion[];
private _index: number = 0;
constructor(questions: TriviaQuestion[]) {
this.questions = questions; // ✓ — assignment in constructor is allowed
}
mutate(): void {
this.questions = []; // Error: Cannot assign to 'questions' because it is a read-only property
}
}

questions is assigned once in the constructor and must never change. readonly enforces this — preventing a subtle bug where a method accidentally replaces the entire questions array mid-quiz.

TypeScript offers a shorthand that declares and initializes a property in one place — the access modifier in the constructor parameter:

class QuizEngine {
private _index: number = 0;
private _score: number = 0;
constructor(private readonly questions: TriviaQuestion[]) {
// questions is declared, typed, and assigned automatically
}
}

Writing private readonly questions: TriviaQuestion[] in the constructor parameter list:

  1. Declares a property named questions on the class.
  2. Marks it private and readonly.
  3. Assigns the constructor argument to this.questions automatically.

This is purely a TypeScript convenience — the compiled output is identical to the explicit version.

In AceIt’s quiz.ts, the access modifiers communicate intent clearly:

class QuizEngine {
private readonly questions: TriviaQuestion[];
private _index: number = 0;
private _score: number = 0;
constructor(questions: TriviaQuestion[]) {
this.questions = questions;
}
// Public read-only access via getters
get total(): number { return this.questions.length; }
get index(): number { return this._index; }
get currentScore(): number { return this._score; }
get isDone(): boolean { return this._index >= this.questions.length; }
get current(): TriviaQuestion | null { return this.questions[this._index] ?? null; }
// Public mutation via a controlled method
answer(selected: string): boolean {
const correct = this.current?.correctAnswer === selected;
if (correct) this._score++;
this._index++;
return correct;
}
// Static utilities
static loadHighScore(): HighScore | null { /* ... */ }
static saveHighScore(score: number, total: number): void { /* ... */ }
static isNewHighScore(score: number, total: number): boolean { /* ... */ }
}

All mutable state is private. External code reads via getters and mutates via answer(). This is a clean encapsulation pattern — the class cannot be put into an inconsistent state from outside.

Update quiz.ts:

  1. Mark questions, _index, and _score as private. Add readonly to questions.
  2. Try accessing engine.questions and engine._index from outside the class. Read the errors, then remove the bad accesses.
  3. Refactor the constructor to use parameter shorthand: constructor(private readonly questions: TriviaQuestion[]). Remove the explicit property declaration for questions and confirm everything still compiles.
  4. Add a protected property debugLabel: string = 'QuizEngine' — observe that it is inaccessible from outside the class but accessible within instance methods.
  • public (default), private, and protected control where a class member is accessible.
  • TypeScript’s private is enforced at compile time only — it can be bypassed at runtime with a cast.
  • JavaScript’s # fields are enforced at runtime — use them when true runtime privacy is required.
  • readonly on a class property allows assignment in the constructor but prevents all later reassignment.
  • Constructor parameter shorthand (private readonly name: Type) declares, marks, and initializes a property in one place.