Skip to content

Building the Quiz Engine with Classes

api.ts fetches and normalizes questions. quiz.ts manages the quiz session — tracking which question is current, whether the player answered correctly, and what the final score is. All mutable state lives here, behind an access-controlled interface.

QuizEngine is constructed with a TriviaQuestion[] and exposes its state through typed getters. External code — main.ts — calls engine.answer(selected) to advance the quiz and reads engine.current, engine.isDone, and engine.currentScore to decide what to render. Nothing outside the class can directly modify _index or _score.

The static methods — loadHighScore, saveHighScore, isNewHighScore — are standalone utilities that belong conceptually with the quiz but do not need an instance.

export class QuizEngine {
private readonly questions: TriviaQuestion[];
private _index: number = 0;
private _score: number = 0;
constructor(questions: TriviaQuestion[]) {
this.questions = questions;
}
  • private readonly questions — assigned once in the constructor, never changeable. Module 05 (readonly).
  • private _index and private _score — mutable state, visible only within the class. Module 04 (access modifiers).
  • Inline defaults (= 0) initialize both without constructor assignments. Module 04 (class properties).

Getters expose controlled read access with explicit return types:

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;
}

current returns TriviaQuestion | null — when _index is out of bounds, questions[_index] is undefined, and ?? null converts it to null. The explicit return type makes the null case visible to every caller. Module 06 (?? narrowing).

answer(selected: string): boolean {
const correct = this.current?.correctAnswer === selected;
if (correct) this._score++;
this._index++;
return correct;
}

this.current?.correctAnswer uses optional chaining — if current is null (quiz already done), the expression short-circuits to undefined, and the comparison returns false. Calling answer after the quiz is done increments _index further, but isDone stays true — a harmless no-op.

Returns boolean — the caller uses this to decide whether to show a “correct” or “incorrect” visual.

static loadHighScore(): HighScore | null {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
try {
const parsed: unknown = JSON.parse(raw);
return isHighScore(parsed) ? parsed : null;
} catch {
return null;
}
}
static saveHighScore(score: number, total: number): void {
const entry: HighScore = {
score,
total,
date: new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }),
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(entry));
}
static isNewHighScore(score: number, total: number): boolean {
const existing = QuizEngine.loadHighScore();
if (!existing) return true;
return score / total > existing.score / existing.total;
}

loadHighScore parses localStorage as unknown and validates with isHighScore before returning — the safe pattern from Module 06 (type predicates). No blind cast.

saveHighScore constructs a HighScore object inline — TypeScript checks that all three fields are present and correctly typed before JSON.stringify.

Add this function above the class in quiz.ts:

function isHighScore(value: unknown): value is HighScore {
return (
typeof value === 'object' &&
value !== null &&
'score' in value && typeof (value as Record<string, unknown>).score === 'number' &&
'total' in value && typeof (value as Record<string, unknown>).total === 'number' &&
'date' in value && typeof (value as Record<string, unknown>).date === 'string'
);
}

When this returns true, TypeScript narrows value to HighScore at the call site — enabling loadHighScore to return parsed directly. Module 06 (type predicates, in operator).

import type { TriviaQuestion, HighScore } from './types.js';
const STORAGE_KEY = 'aceit_highscore';
function isHighScore(value: unknown): value is HighScore {
return (
typeof value === 'object' && value !== null &&
'score' in value && typeof (value as Record<string, unknown>).score === 'number' &&
'total' in value && typeof (value as Record<string, unknown>).total === 'number' &&
'date' in value && typeof (value as Record<string, unknown>).date === 'string'
);
}
export class QuizEngine {
private readonly 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; }
get isDone(): boolean { return this._index >= this.questions.length; }
get current(): TriviaQuestion | null {
return this.questions[this._index] ?? null;
}
answer(selected: string): boolean {
const correct = this.current?.correctAnswer === selected;
if (correct) this._score++;
this._index++;
return correct;
}
static loadHighScore(): HighScore | null {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
try {
const parsed: unknown = JSON.parse(raw);
return isHighScore(parsed) ? parsed : null;
} catch {
return null;
}
}
static saveHighScore(score: number, total: number): void {
const entry: HighScore = {
score,
total,
date: new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }),
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(entry));
}
static isNewHighScore(score: number, total: number): boolean {
const existing = QuizEngine.loadHighScore();
if (!existing) return true;
return score / total > existing.score / existing.total;
}
}
  1. Replace the quiz.ts stub with the complete file above.
  2. Run npm run build — fix any errors.
  3. Test QuizEngine in isolation in the browser console. Import it via window assignment and:
    • Construct an engine with a hardcoded question array.
    • Call engine.answer('wrong') — confirm isDone and currentScore update correctly.
    • Call QuizEngine.saveHighScore(7, 10) — check Application → localStorage in DevTools.
    • Call QuizEngine.loadHighScore() — confirm it returns the saved object.
  • private readonly questions prevents the questions array from being replaced after construction.
  • private _index and _score are only accessible via the public getters — external code cannot put the engine in an inconsistent state.
  • current returns TriviaQuestion | null — the null case is explicit, forcing callers to handle it.
  • isHighScore is a type predicate — it validates localStorage data as unknown before any cast.
  • Static methods belong to the class conceptually but do not require an instance.