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
switchnarrowing and aneverchecklist. - 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.
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
Questiontype is a discriminated union. - Every question has
kind,id,prompt, andpoints. - Each question kind has only the fields it needs.
- Uncommenting
brokenQuestionfails 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:
findAnswerreturnsSubmittedAnswer | undefined.hasSubmittedAnswernarrows awayundefined.- 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:
gradeQuestionusesswitch (question.kind).- Multiple-choice answers use
correctIndex. - True/false answers reject text that is not
trueorfalse. - Fill-in answers use
acceptedAnswers. - The
defaultbranch assigns the leftover question tonever.
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
q3fromvoidtoneverchanges the score to4/4. - Changing a question's
pointschanges 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 theneverchecklist guide every required edit. - Add a
categoryfield so the report can printTypes,Functions, orNarrowing. - 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.