Skip to content

Modeling the Data: Interfaces and Enums

With the scaffold in place, start with data. types.ts is the first file you fill in — every other module imports from it. Writing types before implementation ensures that the shape of every value in the app is agreed upon before any function tries to use it.

AceIt needs types for six things:

  1. The difficulty levels the API accepts
  2. The raw API response — the JSON the browser receives
  3. The normalized question the app works with internally
  4. The full API envelope including response code
  5. The high score stored in localStorage
  6. The quiz state — which screen is active and what data it carries
export type Difficulty = 'easy' | 'medium' | 'hard';

A literal union — not string. Passing 'expert' to any function that expects Difficulty is a compile error, not a silent API failure.

export type ScreenId =
| 'start-screen'
| 'loading-screen'
| 'question-screen'
| 'result-screen';

Constrains showScreen to the four IDs that actually exist in the HTML. Typos are errors.

RawQuestion mirrors exactly what the Open Trivia DB API returns — snake_case property names, HTML-encoded strings, an array of three incorrect answers:

export interface RawQuestion {
type: string;
difficulty: string;
category: string;
question: string;
correct_answer: string;
incorrect_answers: string[];
}
export interface ApiResponse {
response_code: number;
results: RawQuestion[];
}

Declaring these interfaces means TypeScript can verify that fetchJson<ApiResponse> produces something with response_code and results — and that results[0].correct_answer is a string, not any.

TriviaQuestion is what the app works with after normalization. It uses camelCase, readonly on every field (the data is fixed after normalization), and the Difficulty type alias for precision:

export interface TriviaQuestion {
readonly question: string;
readonly category: string;
readonly difficulty: Difficulty;
readonly correctAnswer: string;
readonly allAnswers: string[];
}

The readonly fields communicate that once a question is normalized from the API response, none of its data should change. TypeScript enforces this across the entire codebase.

export interface HighScore {
score: number;
total: number;
date: string;
}

Serialized to localStorage as JSON. QuizEngine.loadHighScore parses and validates against this shape before returning it.

QuizState models the four states the quiz can be in. Each member has a unique status literal — the discriminant — and carries only the data relevant to that state:

export type QuizState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'active'; question: TriviaQuestion; index: number; total: number }
| { status: 'done'; score: number; total: number };

score only exists when status === 'done'. question only exists when status === 'active'. Accessing either in the wrong state is a compile error — invalid states are unrepresentable.

The Open Trivia DB API returns a numeric response_code. The enum replaces magic numbers with readable names:

export enum ResponseCode {
Success = 0,
NoResults = 1,
InvalidParam = 2,
TokenNotFound = 3,
TokenEmpty = 4,
}

data.response_code === ResponseCode.Success is readable. data.response_code === 0 is not.

// ── Type aliases ───────────────────────────────────
export type Difficulty = 'easy' | 'medium' | 'hard';
export type ScreenId =
| 'start-screen'
| 'loading-screen'
| 'question-screen'
| 'result-screen';
export type QuizState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'active'; question: TriviaQuestion; index: number; total: number }
| { status: 'done'; score: number; total: number };
// ── API response shapes ────────────────────────────
export interface RawQuestion {
type: string;
difficulty: string;
category: string;
question: string;
correct_answer: string;
incorrect_answers: string[];
}
export interface ApiResponse {
response_code: number;
results: RawQuestion[];
}
// ── App data shapes ────────────────────────────────
export interface TriviaQuestion {
readonly question: string;
readonly category: string;
readonly difficulty: Difficulty;
readonly correctAnswer: string;
readonly allAnswers: string[];
}
export interface HighScore {
score: number;
total: number;
date: string;
}
// ── Enum ───────────────────────────────────────────
export enum ResponseCode {
Success = 0,
NoResults = 1,
InvalidParam = 2,
TokenNotFound = 3,
TokenEmpty = 4,
}
  1. Replace the types.ts stub with the complete file above.
  2. Run npm run build — confirm it compiles with no errors.
  3. For each exported type, identify which module introduced the concept it uses:
    • Difficulty → Module ___
    • RawQuestion → Module ___
    • readonly correctAnswer → Module ___
    • QuizState → Module ___
    • ResponseCode → Module ___
  4. Try adding a fifth status: 'paused' member to QuizState. Note that this compiles fine — it only becomes a problem in the switch statement (Lesson 06).
  • types.ts is the single source of truth — every other file imports from it.
  • Separate RawQuestion (API shape) from TriviaQuestion (app shape) — the boundary between them is the normalization step in api.ts.
  • readonly on TriviaQuestion fields prevents accidental mutation after normalization.
  • QuizState as a discriminated union makes invalid states unrepresentable.
  • ResponseCode as an enum replaces the API’s magic numbers with readable names.
  • Write types first — agreed-upon shapes make every subsequent implementation step concrete.