Module Recap
Module 04 typed the functions and classes that drive AceIt’s logic. Every boundary in the app is now annotated — what goes in, what comes out, what is visible, and what cannot change.
What you learned
Section titled “What you learned”Typed parameters and return types — every function parameter is annotated. Return types are inferred for simple functions and declared explicitly for async functions and module public surfaces. void for no return, never for functions that always throw.
Optional, default, and rest parameters — ? marks a parameter as T | undefined, requiring a guard before use. Defaults provide fallback values and are never undefined inside the body. Rest parameters (...name: T[]) collect remaining arguments into a typed array.
Function types and callbacks — functions are values. A function type expression ((param: T) => R) names a callable type. Contextual typing infers callback parameter types from the expected function type — no annotation needed at the call site.
Typed class properties — properties are declared at the top of the class body. Inline defaults eliminate constructor assignments for simple initial values. Getters, instance methods, and static methods are annotated like standalone functions.
Access modifiers and readonly — private restricts a member to the class body. protected extends that to subclasses. readonly prevents reassignment after construction. Constructor parameter shorthand combines declaration, access control, and initialization in one line.
The complete quiz.ts
Section titled “The complete quiz.ts”import type { TriviaQuestion, HighScore } from './types.js';
const STORAGE_KEY = 'aceit_highscore';
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 { return JSON.parse(raw) as HighScore; } 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; }}The complete api.ts (non-generic version)
Section titled “The complete api.ts (non-generic version)”import type { ApiResponse, RawQuestion, TriviaQuestion, Difficulty } from './types.js';import { ResponseCode } from './types.js';
const BASE_URL = 'https://opentdb.com/api.php';
async function fetchJson(url: string): Promise<unknown> { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`); return response.json();}
function decodeHtml(html: string): string { const el = document.createElement('textarea'); el.innerHTML = html; return el.value;}
function shuffle(arr: string[]): string[] { return [...arr].sort(() => Math.random() - 0.5);}
function normalizeQuestion(raw: RawQuestion): TriviaQuestion { return { question: decodeHtml(raw.question), category: decodeHtml(raw.category), difficulty: raw.difficulty as Difficulty, correctAnswer: decodeHtml(raw.correct_answer), allAnswers: shuffle([raw.correct_answer, ...raw.incorrect_answers]).map(decodeHtml), };}
export async function fetchQuestions( amount: number = 10, difficulty?: Difficulty,): Promise<TriviaQuestion[]> { let url = `${BASE_URL}?amount=${amount}&type=multiple`; if (difficulty) url += `&difficulty=${difficulty}`;
const data = await fetchJson(url) as ApiResponse;
if (data.response_code === ResponseCode.NoResults) { throw new Error('Not enough questions available. Try a different setting.'); } if (data.response_code !== ResponseCode.Success) { throw new Error(`API error (code ${data.response_code})`); }
return data.results.map(normalizeQuestion);}Notice that fetchJson returns Promise<unknown> — the caller casts to ApiResponse. Module 05 replaces this with a generic fetchJson<T> that carries the type through without a cast.
Key terms
Section titled “Key terms”| Term | What it means |
|---|---|
void | Return type for functions that return nothing |
never | Return type for functions that never return (always throw) |
| Contextual typing | TypeScript inferring a type from the position where a value is expected |
| Default parameter | A parameter with a fallback value — never undefined inside the function |
| Rest parameter | ...name: T[] — collects remaining arguments into a typed array |
private | Class member visible only within the class body (compile-time only) |
protected | Class member visible within the class and its subclasses |
readonly | Property that can only be assigned in the constructor |
| Constructor shorthand | constructor(private readonly name: Type) — declares, marks, and initializes in one step |
What is next
Section titled “What is next”Module 05 introduces generics — the feature that makes fetchJson<T> work. Instead of returning unknown and casting, fetchJson<T> carries the expected type as a parameter through the function, giving the caller a fully typed result with no unsafe casts.