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.
The three access modifiers
Section titled “The three access modifiers”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 privateengine.questions; // Error: Property 'questions' is privateengine.total; // ✓ — getter is publicThe 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:
| Feature | TypeScript private | JavaScript # |
|---|---|---|
| Enforced at | Compile time only | Runtime + compile time |
| Accessible at runtime? | Yes — via (obj as any).prop | No — truly private |
| Appears in compiled JS? | Yes, as a regular property | Yes, 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 on class properties
Section titled “readonly on class properties”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.
Constructor parameter shorthand
Section titled “Constructor parameter shorthand”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:
- Declares a property named
questionson the class. - Marks it
privateandreadonly. - Assigns the constructor argument to
this.questionsautomatically.
This is purely a TypeScript convenience — the compiled output is identical to the explicit version.
The complete QuizEngine access structure
Section titled “The complete QuizEngine access structure”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.
Exercise
Section titled “Exercise”Update quiz.ts:
- Mark
questions,_index, and_scoreasprivate. Addreadonlytoquestions. - Try accessing
engine.questionsandengine._indexfrom outside the class. Read the errors, then remove the bad accesses. - Refactor the constructor to use parameter shorthand:
constructor(private readonly questions: TriviaQuestion[]). Remove the explicit property declaration forquestionsand confirm everything still compiles. - Add a
protectedpropertydebugLabel: string = 'QuizEngine'— observe that it is inaccessible from outside the class but accessible within instance methods.
public(default),private, andprotectedcontrol where a class member is accessible.- TypeScript’s
privateis 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. readonlyon 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.