Skip to content

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.

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 readonlyprivate 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.

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

TermWhat it means
voidReturn type for functions that return nothing
neverReturn type for functions that never return (always throw)
Contextual typingTypeScript inferring a type from the position where a value is expected
Default parameterA parameter with a fallback value — never undefined inside the function
Rest parameter...name: T[] — collects remaining arguments into a typed array
privateClass member visible only within the class body (compile-time only)
protectedClass member visible within the class and its subclasses
readonlyProperty that can only be assigned in the constructor
Constructor shorthandconstructor(private readonly name: Type) — declares, marks, and initializes in one step

Module 05 — Generics →

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.