Fetching and Typing API Data
With types in place, implement api.ts — the module responsible for fetching questions from the Open Trivia DB and transforming the raw response into the normalized TriviaQuestion shape the app uses.
What api.ts contains
Section titled “What api.ts contains”fetchJson<T>— a genericfetchwrapper that returnsPromise<T>(Module 05)decodeHtml— decodes HTML entities in API strings (Module 04)shuffle<T>— shuffles an array without mutating the original (Module 05)normalizeQuestion— convertsRawQuestion→TriviaQuestion(Modules 03, 04)fetchQuestions— builds the URL, fetches, validates, and maps (Modules 04, 05, 06)
The generic fetch wrapper
Section titled “The generic fetch wrapper”export async function fetchJson<T>(url: string): Promise<T> { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`); return response.json() as Promise<T>;}The one as Promise<T> cast lives here — at the boundary with the untyped browser fetch API. Every caller gets a fully typed result with no cast of their own. fetchJson<ApiResponse>(url) returns Promise<ApiResponse>.
Decoding HTML entities
Section titled “Decoding HTML entities”The Open Trivia DB returns HTML-encoded strings — ampersands become &, quotes become ". A textarea element decodes them reliably across all browsers:
function decodeHtml(html: string): string { const el = document.createElement('textarea'); el.innerHTML = html; return el.value;}Parameter: string. Return: string. The DOM library (lib: ["ES2020", "DOM"]) makes document.createElement available to the type checker.
Generic shuffle
Section titled “Generic shuffle”The Module 04 version only worked with string[]. The generic version works with any array type:
function shuffle<T>(arr: T[]): T[] { return [...arr].sort(() => Math.random() - 0.5);}T is inferred from the input. The spread ([...arr]) creates a copy before sorting — the original array is not mutated. normalizeQuestion passes string[]; TypeScript infers T as string.
Normalizing a question
Section titled “Normalizing a question”normalizeQuestion takes the API shape and returns the app shape — converting snake_case to camelCase, decoding HTML entities, and shuffling the answer options:
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), };}raw.difficulty as Difficulty is a narrow cast — the API returns 'easy', 'medium', or 'hard' as strings, which TypeScript infers as string. The cast narrows it to the Difficulty literal union. If the API ever returns an unexpected value, the cast would be wrong — a type predicate on difficulty would catch it, but for this project the API contract is trusted.
allAnswers combines the one correct and three incorrect answers into a shuffled array of four. This is the array the UI renders as answer buttons.
Fetching questions
Section titled “Fetching questions”const BASE_URL = 'https://opentdb.com/api.php';
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<ApiResponse>(url);
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);}amount = 10— default parameter from Module 04.difficulty?— optional parameter,Difficulty | undefined. Appended only when present.fetchJson<ApiResponse>— generic call from Module 05.datais fully typed asApiResponse.ResponseCode.NoResultsandResponseCode.Success— enum members from Module 02.data.results.map(normalizeQuestion)— returnsTriviaQuestion[]. TypeScript infers this from the return type ofnormalizeQuestion.
The complete api.ts
Section titled “The complete api.ts”import type { ApiResponse, RawQuestion, TriviaQuestion, Difficulty } from './types.js';import { ResponseCode } from './types.js';
const BASE_URL = 'https://opentdb.com/api.php';
export async function fetchJson<T>(url: string): Promise<T> { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`); return response.json() as Promise<T>;}
function decodeHtml(html: string): string { const el = document.createElement('textarea'); el.innerHTML = html; return el.value;}
function shuffle<T>(arr: T[]): T[] { 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<ApiResponse>(url);
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);}Exercise
Section titled “Exercise”- Replace the
api.tsstub with the complete file above. - Run
npm run build— fix any errors before moving on. - In the browser DevTools console, after loading the page, manually call
fetchQuestions:Call// Modules are not accessible from the console directly — add a temporary window assignment in main.ts:// import { fetchQuestions } from './api.js';// (window as any).fetchQuestions = fetchQuestions;await window.fetchQuestions(5, 'easy')and inspect the result. Confirm each item hasquestion,correctAnswer, andallAnswerswith four strings. - Remove the
windowassignment after testing.
fetchJson<T>carries the expected type through without requiring casts at call sites — one cast at the boundary, zero downstream.decodeHtmluses atextareaelement — the most reliable cross-browser HTML entity decoder.shuffle<T>uses a spread to avoid mutating the input array.normalizeQuestionis the boundary between the API’s world and the app’s world — snake_case to camelCase, HTML decoded, answers shuffled.fetchQuestionsvalidates the response code with theResponseCodeenum before mapping results.