The Type-Safe Quiz Engine

Beginner · 90 min build · 🏆 milestone project

You are ready to build a small quiz engine, a program that stores questions, checks submitted answers, and prints a score report. The goal is not a fancy interface. The goal is to make wrong question shapes impossible and scoring logic easy to trust.

The playground is standing in for command-line output. It uses fixed sample answers so every run is repeatable.

What you'll build

You will build one quiz workspace with:

  • Three question shapes: multiple choice, true/false, and fill-in.
  • A discriminated union that keeps each question's fields honest.
  • A scorer that uses switch narrowing and a never checklist.
  • A tiny type predicate for submitted answers.
  • A sample run that prints per-question feedback and the final score.

A scaffold is starter code with comments that show where each part belongs. The acceptance criteria are checks you can run yourself to prove a stage works.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

Checkpoint 1: model the question set

Start with the question types. A multiple-choice question needs choices and a correct index. A true/false question needs a boolean answer. A fill-in question needs accepted text answers.

Acceptance criteria:

  • The Question type is a discriminated union.
  • Every question has kind, id, prompt, and points.
  • Each question kind has only the fields it needs.
  • Uncommenting brokenQuestion fails before the program runs.

Checkpoint 2: find and clean answers

The submitted answers are fixed data for now. That keeps the project focused on types instead of input timing.

Acceptance criteria:

  • findAnswer returns SubmittedAnswer | undefined.
  • hasSubmittedAnswer narrows away undefined.
  • Blank answers count as missing.
  • Fill-in answers compare lowercase trimmed text.

Checkpoint 3: score with narrowing

The scorer is the heart of the project. Each case is a safe room: TypeScript knows the exact question shape in that branch.

Acceptance criteria:

  • gradeQuestion uses switch (question.kind).
  • Multiple-choice answers use correctIndex.
  • True/false answers reject text that is not true or false.
  • Fill-in answers use acceptedAnswers.
  • The default branch assigns the leftover question to never.

Checkpoint 4: print the report

A score report is the final summary of each graded answer plus the total. This stage proves your engine works end to end.

Acceptance criteria:

  • The first run prints three result lines.
  • The sample score is 3/4.
  • Changing q3 from void to never changes the score to 4/4.
  • Changing a question's points changes the total everywhere.

Stretch goals

Choose one after the base engine works:

  • Add a second multiple-choice question with four choices.
  • Add a missing answer by removing one item from submittedAnswers.
  • Add a "short_answer" question kind and let the never checklist guide every required edit.
  • Add a category field so the report can print Types, Functions, or Narrowing.
  • Move the sample answers into a different order and confirm lookup still uses questionId.

How to get unstuck

If a union member rejects a field, revisit the literal-union model from Union Types & Literal Types. If a branch cannot use a field yet, compare it to Narrowing: How TypeScript Thinks. If the switch feels noisy, reread Discriminated Unions and follow the never checklist. If SubmittedAnswer | undefined is confusing, use Type Predicates & in / instanceof to trace the answer is SubmittedAnswer promise.

Part I is complete when this engine feels readable. Next, Part II moves deeper into object types, classes, and generics so your shapes and reusable code can grow without giving up the checker.

+50 XP on completion