Project 7.2: Quiz Game
Duration: 2 hours
Project Overview
Build an interactive quiz game that tests users on various topics. The game will feature multiple-choice questions, scoring, different difficulty levels, and category selection. This project focuses on using interfaces to model complex data structures and implementing game logic with TypeScript.
Learning Objectives
By completing this project, you will practice:
- Designing complex data models with nested interfaces
- Using union types and literal types for constrained values
- Implementing game state management
- Working with arrays and randomization
- Building an interactive CLI experience
- Handling edge cases and user input validation
Requirements
Functional Requirements
- Category Selection: Choose from multiple quiz categories
- Difficulty Levels: Easy, Medium, and Hard questions
- Multiple Choice: 4 answer options per question
- Scoring System: Points based on difficulty and time
- Progress Tracking: Show current question and score
- Results Summary: Display final score with statistics
Game Flow
=== TypeScript Quiz ===
Select a category:
1. JavaScript Basics
2. TypeScript Types
3. Functions and Classes
4. Mixed (All categories)
Your choice: 2
Select difficulty:
1. Easy
2. Medium
3. Hard
4. All difficulties
Your choice: 2
Starting quiz: TypeScript Types (Medium)
5 questions, 10 points each
---
Question 1/5
What keyword is used to define a type alias in TypeScript?
1. interface
2. type
3. alias
4. typedef
Your answer: 2
Correct! +10 points
---
Question 2/5
...
Data Models
Define these types for your quiz game:
// types.ts
/**
* Difficulty levels for questions
*/
type Difficulty = 'easy' | 'medium' | 'hard';
/**
* Available quiz categories
*/
type Category = 'javascript' | 'typescript-types' | 'functions-classes';
/**
* A single quiz question
*/
interface Question {
id: number;
category: Category;
difficulty: Difficulty;
text: string;
options: string[];
correctAnswer: number; // Index of correct option (0-3)
explanation: string;
}
/**
* Result of answering a single question
*/
interface AnswerResult {
questionId: number;
userAnswer: number;
correct: boolean;
pointsEarned: number;
timeTaken: number; // seconds
}
/**
* Complete quiz session results
*/
interface QuizResult {
category: Category | 'mixed';
difficulty: Difficulty | 'all';
totalQuestions: number;
correctAnswers: number;
totalPoints: number;
maxPoints: number;
answers: AnswerResult[];
startTime: Date;
endTime: Date;
}
/**
* Quiz configuration
*/
interface QuizConfig {
questionsPerQuiz: number;
pointsByDifficulty: Record<Difficulty, number>;
timeBonusEnabled: boolean;
}
Project Structure
quiz-game/
├── src/
│ ├── index.ts # Entry point
│ ├── types.ts # Type definitions
│ ├── questions.ts # Question bank
│ ├── quiz-engine.ts # Quiz logic
│ ├── scoring.ts # Scoring system
│ └── ui.ts # User interface
├── package.json
└── tsconfig.json
Implementation Guide
Step 1: Define Types
// src/types.ts
export type Difficulty = 'easy' | 'medium' | 'hard';
export type Category = 'javascript' | 'typescript-types' | 'functions-classes';
export interface Question {
id: number;
category: Category;
difficulty: Difficulty;
text: string;
options: string[];
correctAnswer: number;
explanation: string;
}
export interface AnswerResult {
questionId: number;
userAnswer: number;
correct: boolean;
pointsEarned: number;
}
export interface QuizResult {
category: Category | 'mixed';
difficulty: Difficulty | 'all';
totalQuestions: number;
correctAnswers: number;
totalPoints: number;
maxPoints: number;
answers: AnswerResult[];
}
export interface QuizConfig {
questionsPerQuiz: number;
pointsByDifficulty: Record<Difficulty, number>;
}
// Category display names
export const categoryNames: Record<Category | 'mixed', string> = {
javascript: 'JavaScript Basics',
'typescript-types': 'TypeScript Types',
'functions-classes': 'Functions and Classes',
mixed: 'Mixed (All Categories)',
};
// Difficulty display names
export const difficultyNames: Record<Difficulty | 'all', string> = {
easy: 'Easy',
medium: 'Medium',
hard: 'Hard',
all: 'All Difficulties',
};
Step 2: Create Question Bank
// src/questions.ts
import { Question } from './types';
export const questions: Question[] = [
// JavaScript Basics - Easy
{
id: 1,
category: 'javascript',
difficulty: 'easy',
text: 'Which keyword declares a variable that cannot be reassigned?',
options: ['var', 'let', 'const', 'static'],
correctAnswer: 2,
explanation:
'const declares a constant variable that cannot be reassigned after initialization.',
},
{
id: 2,
category: 'javascript',
difficulty: 'easy',
text: 'What is the result of typeof null?',
options: ['null', 'undefined', 'object', 'boolean'],
correctAnswer: 2,
explanation: "This is a known JavaScript quirk - typeof null returns 'object'.",
},
{
id: 3,
category: 'javascript',
difficulty: 'easy',
text: 'Which method adds an element to the end of an array?',
options: ['unshift()', 'push()', 'pop()', 'shift()'],
correctAnswer: 1,
explanation: 'push() adds elements to the end, unshift() adds to the beginning.',
},
// JavaScript Basics - Medium
{
id: 4,
category: 'javascript',
difficulty: 'medium',
text: 'What does the spread operator (...) do with arrays?',
options: [
'Removes duplicates',
'Reverses the array',
'Expands array into individual elements',
'Sorts the array',
],
correctAnswer: 2,
explanation: 'The spread operator expands an iterable into individual elements.',
},
{
id: 5,
category: 'javascript',
difficulty: 'medium',
text: 'What is the difference between == and ===?',
options: [
'No difference',
'=== checks value only, == checks type too',
'== checks value only, === checks value and type',
'=== is for strings only',
],
correctAnswer: 2,
explanation: '== performs type coercion, === checks both value and type without coercion.',
},
// JavaScript Basics - Hard
{
id: 6,
category: 'javascript',
difficulty: 'hard',
text: 'What is a closure in JavaScript?',
options: [
'A way to close a program',
'A function with access to its outer scope variables',
'A method to close database connections',
'An error handling mechanism',
],
correctAnswer: 1,
explanation:
'A closure is a function that remembers and can access variables from its outer scope.',
},
// TypeScript Types - Easy
{
id: 7,
category: 'typescript-types',
difficulty: 'easy',
text: 'Which is NOT a basic TypeScript type?',
options: ['string', 'number', 'boolean', 'integer'],
correctAnswer: 3,
explanation:
"TypeScript uses 'number' for all numeric values, there is no separate 'integer' type.",
},
{
id: 8,
category: 'typescript-types',
difficulty: 'easy',
text: 'How do you specify that a variable is a string?',
options: ['let name = string;', 'let name: string;', 'let string name;', 'string let name;'],
correctAnswer: 1,
explanation: 'Type annotations in TypeScript use a colon after the variable name.',
},
{
id: 9,
category: 'typescript-types',
difficulty: 'easy',
text: 'What type should you use when a value can be anything?',
options: ['unknown', 'any', 'void', 'never'],
correctAnswer: 1,
explanation: "The 'any' type disables type checking for that value.",
},
// TypeScript Types - Medium
{
id: 10,
category: 'typescript-types',
difficulty: 'medium',
text: 'What is a union type?',
options: [
'A type that combines multiple types into one',
'A type that can be one of several specified types',
'A type for mathematical unions',
'A type for joining strings',
],
correctAnswer: 1,
explanation: 'Union types allow a value to be one of several types, written as Type1 | Type2.',
},
{
id: 11,
category: 'typescript-types',
difficulty: 'medium',
text: 'What keyword defines a type alias?',
options: ['interface', 'type', 'alias', 'define'],
correctAnswer: 1,
explanation: "The 'type' keyword creates a type alias, e.g., type Name = string.",
},
{
id: 12,
category: 'typescript-types',
difficulty: 'medium',
text: 'What is the difference between interface and type?',
options: [
'They are exactly the same',
'Interfaces can be extended, types cannot',
'Interfaces can be extended and merged, types are more flexible',
'Types are for objects only',
],
correctAnswer: 2,
explanation:
'Interfaces support declaration merging and extending, while types can represent unions and primitives.',
},
// TypeScript Types - Hard
{
id: 13,
category: 'typescript-types',
difficulty: 'hard',
text: "What does the 'never' type represent?",
options: [
'A type that is never used',
'A type for functions that never return',
'An alias for undefined',
'A deprecated type',
],
correctAnswer: 1,
explanation:
"The 'never' type represents values that never occur, like functions that always throw.",
},
{
id: 14,
category: 'typescript-types',
difficulty: 'hard',
text: 'What is a literal type?',
options: [
'A type for string literals only',
'A type that accepts only a specific value',
'A type for literary content',
'A type for constants',
],
correctAnswer: 1,
explanation:
"Literal types restrict a value to a specific literal, e.g., type Direction = 'left' | 'right'.",
},
// Functions and Classes - Easy
{
id: 15,
category: 'functions-classes',
difficulty: 'easy',
text: "How do you specify a function's return type?",
options: [
'function name() -> string',
'function name(): string',
'function name() returns string',
'string function name()',
],
correctAnswer: 1,
explanation: 'Return types are specified after the parameter list with a colon.',
},
{
id: 16,
category: 'functions-classes',
difficulty: 'easy',
text: 'What is an arrow function?',
options: [
'A function that returns arrows',
'A shorter syntax for writing functions',
'A function that only points to other functions',
'A deprecated function type',
],
correctAnswer: 1,
explanation: 'Arrow functions provide a concise syntax: const add = (a, b) => a + b.',
},
// Functions and Classes - Medium
{
id: 17,
category: 'functions-classes',
difficulty: 'medium',
text: "What does the 'private' modifier do in a class?",
options: [
'Makes the property slower',
'Restricts access to within the class only',
'Makes the property optional',
'Encrypts the property value',
],
correctAnswer: 1,
explanation: 'Private members can only be accessed from within the class that defines them.',
},
{
id: 18,
category: 'functions-classes',
difficulty: 'medium',
text: 'What is method overloading?',
options: [
'Having too many methods',
'Defining multiple signatures for the same method name',
'Making a method work too hard',
'Copying methods from another class',
],
correctAnswer: 1,
explanation:
'Method overloading allows a function to have multiple signatures with different parameters.',
},
// Functions and Classes - Hard
{
id: 19,
category: 'functions-classes',
difficulty: 'hard',
text: 'What is the purpose of abstract classes?',
options: [
'To define classes that cannot be instantiated directly',
'To make classes harder to understand',
'To hide implementation details',
'To improve performance',
],
correctAnswer: 0,
explanation: 'Abstract classes serve as base classes and cannot be instantiated directly.',
},
{
id: 20,
category: 'functions-classes',
difficulty: 'hard',
text: 'What is a generic function?',
options: [
'A function that does everything',
'A function that works with multiple types using type parameters',
'A function without a specific purpose',
'A function that returns generic objects',
],
correctAnswer: 1,
explanation:
'Generic functions use type parameters to work with multiple types while maintaining type safety.',
},
];
/**
* Gets questions filtered by category and difficulty
*/
export function getQuestions(category: string, difficulty: string): Question[] {
return questions.filter((q) => {
const categoryMatch = category === 'mixed' || q.category === category;
const difficultyMatch = difficulty === 'all' || q.difficulty === difficulty;
return categoryMatch && difficultyMatch;
});
}
/**
* Shuffles an array randomly
*/
export function shuffleArray<T>(array: T[]): T[] {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
Step 3: Create Scoring System
// src/scoring.ts
import { AnswerResult, Difficulty, QuizConfig, QuizResult } from './types';
export const defaultConfig: QuizConfig = {
questionsPerQuiz: 5,
pointsByDifficulty: {
easy: 5,
medium: 10,
hard: 15,
},
};
/**
* Calculates points for a correct answer
*/
export function calculatePoints(difficulty: Difficulty): number {
return defaultConfig.pointsByDifficulty[difficulty];
}
/**
* Calculates the maximum possible points for a quiz
*/
export function calculateMaxPoints(difficulties: Difficulty[], questionCount: number): number {
if (difficulties.length === 0) return 0;
// Average points if mixed difficulties
const avgPoints =
difficulties.reduce((sum, d) => sum + defaultConfig.pointsByDifficulty[d], 0) /
difficulties.length;
return Math.round(avgPoints * questionCount);
}
/**
* Calculates the grade based on percentage
*/
export function calculateGrade(percentage: number): string {
if (percentage >= 90) return 'A - Excellent!';
if (percentage >= 80) return 'B - Great job!';
if (percentage >= 70) return 'C - Good effort!';
if (percentage >= 60) return 'D - Keep practicing!';
return 'F - Try again!';
}
/**
* Generates a summary of quiz results
*/
export function generateSummary(result: QuizResult): string {
const percentage = Math.round((result.correctAnswers / result.totalQuestions) * 100);
const grade = calculateGrade(percentage);
const lines = [
'',
'========================================',
' QUIZ COMPLETE! ',
'========================================',
'',
`Category: ${result.category}`,
`Difficulty: ${result.difficulty}`,
'',
`Correct Answers: ${result.correctAnswers}/${result.totalQuestions}`,
`Score: ${result.totalPoints}/${result.maxPoints} points`,
`Percentage: ${percentage}%`,
'',
`Grade: ${grade}`,
'',
'========================================',
];
return lines.join('\n');
}
/**
* Gets performance by difficulty
*/
export function getPerformanceByDifficulty(
answers: AnswerResult[],
questions: Array<{ id: number; difficulty: Difficulty }>
): Record<Difficulty, { correct: number; total: number }> {
const performance: Record<Difficulty, { correct: number; total: number }> = {
easy: { correct: 0, total: 0 },
medium: { correct: 0, total: 0 },
hard: { correct: 0, total: 0 },
};
for (const answer of answers) {
const question = questions.find((q) => q.id === answer.questionId);
if (question) {
performance[question.difficulty].total++;
if (answer.correct) {
performance[question.difficulty].correct++;
}
}
}
return performance;
}
Step 4: Create Quiz Engine
// src/quiz-engine.ts
import { getQuestions, shuffleArray } from './questions';
import { calculatePoints, defaultConfig } from './scoring';
import { AnswerResult, Category, Difficulty, Question, QuizResult } from './types';
export class QuizEngine {
private questions: Question[] = [];
private currentIndex = 0;
private answers: AnswerResult[] = [];
private category: Category | 'mixed' = 'mixed';
private difficulty: Difficulty | 'all' = 'all';
/**
* Initializes a new quiz
*/
startQuiz(category: Category | 'mixed', difficulty: Difficulty | 'all'): boolean {
this.category = category;
this.difficulty = difficulty;
// Get and shuffle questions
const availableQuestions = getQuestions(category, difficulty);
if (availableQuestions.length === 0) {
return false;
}
// Shuffle and take the configured number of questions
this.questions = shuffleArray(availableQuestions).slice(0, defaultConfig.questionsPerQuiz);
this.currentIndex = 0;
this.answers = [];
return true;
}
/**
* Gets the current question
*/
getCurrentQuestion(): Question | null {
if (this.currentIndex >= this.questions.length) {
return null;
}
return this.questions[this.currentIndex];
}
/**
* Gets the current question number (1-based)
*/
getCurrentQuestionNumber(): number {
return this.currentIndex + 1;
}
/**
* Gets the total number of questions
*/
getTotalQuestions(): number {
return this.questions.length;
}
/**
* Submits an answer for the current question
*/
submitAnswer(answerIndex: number): AnswerResult | null {
const question = this.getCurrentQuestion();
if (!question) return null;
const correct = answerIndex === question.correctAnswer;
const pointsEarned = correct ? calculatePoints(question.difficulty) : 0;
const result: AnswerResult = {
questionId: question.id,
userAnswer: answerIndex,
correct,
pointsEarned,
};
this.answers.push(result);
this.currentIndex++;
return result;
}
/**
* Checks if the quiz is complete
*/
isComplete(): boolean {
return this.currentIndex >= this.questions.length;
}
/**
* Gets the current score
*/
getCurrentScore(): number {
return this.answers.reduce((sum, a) => sum + a.pointsEarned, 0);
}
/**
* Gets the number of correct answers so far
*/
getCorrectCount(): number {
return this.answers.filter((a) => a.correct).length;
}
/**
* Gets the final quiz results
*/
getResults(): QuizResult {
const totalPoints = this.answers.reduce((sum, a) => sum + a.pointsEarned, 0);
const maxPoints = this.questions.reduce((sum, q) => sum + calculatePoints(q.difficulty), 0);
return {
category: this.category,
difficulty: this.difficulty,
totalQuestions: this.questions.length,
correctAnswers: this.getCorrectCount(),
totalPoints,
maxPoints,
answers: this.answers,
};
}
/**
* Gets a question by ID (for result review)
*/
getQuestionById(id: number): Question | undefined {
return this.questions.find((q) => q.id === id);
}
}
Step 5: Create UI Module
// src/ui.ts
import * as readline from 'readline';
import {
AnswerResult,
Category,
Difficulty,
Question,
categoryNames,
difficultyNames,
} from './types';
export function createReadline(): readline.Interface {
return readline.createInterface({
input: process.stdin,
output: process.stdout,
});
}
export function prompt(rl: readline.Interface, question: string): Promise<string> {
return new Promise((resolve) => {
rl.question(question, (answer) => {
resolve(answer.trim());
});
});
}
/**
* Displays the welcome screen
*/
export function showWelcome(): void {
console.log('');
console.log('========================================');
console.log(' TYPESCRIPT QUIZ GAME ');
console.log('========================================');
console.log('');
console.log('Test your TypeScript knowledge!');
console.log('Answer multiple choice questions and');
console.log('see how well you score.');
console.log('');
}
/**
* Displays category selection menu
*/
export function showCategoryMenu(): void {
console.log('Select a category:');
console.log('1. JavaScript Basics');
console.log('2. TypeScript Types');
console.log('3. Functions and Classes');
console.log('4. Mixed (All Categories)');
console.log('');
}
/**
* Displays difficulty selection menu
*/
export function showDifficultyMenu(): void {
console.log('\nSelect difficulty:');
console.log('1. Easy');
console.log('2. Medium');
console.log('3. Hard');
console.log('4. All Difficulties');
console.log('');
}
/**
* Parses category selection
*/
export function parseCategory(input: string): Category | 'mixed' {
switch (input) {
case '1':
return 'javascript';
case '2':
return 'typescript-types';
case '3':
return 'functions-classes';
default:
return 'mixed';
}
}
/**
* Parses difficulty selection
*/
export function parseDifficulty(input: string): Difficulty | 'all' {
switch (input) {
case '1':
return 'easy';
case '2':
return 'medium';
case '3':
return 'hard';
default:
return 'all';
}
}
/**
* Displays a question
*/
export function displayQuestion(
question: Question,
questionNumber: number,
totalQuestions: number
): void {
console.log('');
console.log('----------------------------------------');
console.log(`Question ${questionNumber}/${totalQuestions}`);
console.log(`Difficulty: ${difficultyNames[question.difficulty]}`);
console.log('----------------------------------------');
console.log('');
console.log(question.text);
console.log('');
question.options.forEach((option, index) => {
console.log(`${index + 1}. ${option}`);
});
console.log('');
}
/**
* Displays the result of an answer
*/
export function displayAnswerResult(result: AnswerResult, question: Question): void {
console.log('');
if (result.correct) {
console.log('*** CORRECT! ***');
console.log(`+${result.pointsEarned} points`);
} else {
console.log('*** INCORRECT ***');
console.log(`The correct answer was: ${question.options[question.correctAnswer]}`);
}
console.log('');
console.log(`Explanation: ${question.explanation}`);
}
/**
* Displays current progress
*/
export function displayProgress(correct: number, total: number, score: number): void {
console.log('');
console.log(`Progress: ${correct}/${total} correct | Score: ${score} points`);
}
/**
* Validates answer input
*/
export function isValidAnswer(input: string, optionCount: number): boolean {
const num = parseInt(input, 10);
return !isNaN(num) && num >= 1 && num <= optionCount;
}
/**
* Displays "press enter to continue" prompt
*/
export async function waitForEnter(rl: readline.Interface): Promise<void> {
await prompt(rl, 'Press Enter to continue...');
}
Step 6: Create Main Application
// src/index.ts
import { QuizEngine } from './quiz-engine';
import { generateSummary } from './scoring';
import { categoryNames, difficultyNames } from './types';
import {
createReadline,
displayAnswerResult,
displayProgress,
displayQuestion,
isValidAnswer,
parseCategory,
parseDifficulty,
prompt,
showCategoryMenu,
showDifficultyMenu,
showWelcome,
waitForEnter,
} from './ui';
async function main(): Promise<void> {
const rl = createReadline();
const quizEngine = new QuizEngine();
let playing = true;
while (playing) {
showWelcome();
// Category selection
showCategoryMenu();
const categoryInput = await prompt(rl, 'Your choice: ');
const category = parseCategory(categoryInput);
// Difficulty selection
showDifficultyMenu();
const difficultyInput = await prompt(rl, 'Your choice: ');
const difficulty = parseDifficulty(difficultyInput);
// Start quiz
const started = quizEngine.startQuiz(category, difficulty);
if (!started) {
console.log('\nNo questions available for this selection.');
console.log('Please try a different combination.\n');
continue;
}
console.log('');
console.log('========================================');
console.log(`Category: ${categoryNames[category]}`);
console.log(`Difficulty: ${difficultyNames[difficulty]}`);
console.log(`Questions: ${quizEngine.getTotalQuestions()}`);
console.log('========================================');
await waitForEnter(rl);
// Quiz loop
while (!quizEngine.isComplete()) {
const question = quizEngine.getCurrentQuestion();
if (!question) break;
displayQuestion(
question,
quizEngine.getCurrentQuestionNumber(),
quizEngine.getTotalQuestions()
);
// Get valid answer
let answerInput: string;
do {
answerInput = await prompt(rl, 'Your answer (1-4): ');
if (!isValidAnswer(answerInput, 4)) {
console.log('Please enter a number between 1 and 4.');
}
} while (!isValidAnswer(answerInput, 4));
const answerIndex = parseInt(answerInput, 10) - 1;
const result = quizEngine.submitAnswer(answerIndex);
if (result) {
displayAnswerResult(result, question);
displayProgress(
quizEngine.getCorrectCount(),
quizEngine.getCurrentQuestionNumber() - 1,
quizEngine.getCurrentScore()
);
}
if (!quizEngine.isComplete()) {
await waitForEnter(rl);
}
}
// Show results
const results = quizEngine.getResults();
console.log(generateSummary(results));
// Show detailed results
console.log('\nDetailed Results:');
console.log('----------------------------------------');
for (const answer of results.answers) {
const question = quizEngine.getQuestionById(answer.questionId);
if (question) {
const status = answer.correct ? '[OK]' : '[X]';
console.log(`${status} ${question.text.substring(0, 50)}...`);
}
}
// Play again?
console.log('');
const playAgain = await prompt(rl, 'Play again? (y/n): ');
playing = playAgain.toLowerCase() === 'y';
if (playing) {
console.clear();
}
}
console.log('\nThanks for playing! Keep learning TypeScript!');
rl.close();
}
main().catch(console.error);
Configuration Files
// package.json
{
"name": "quiz-game",
"version": "1.0.0",
"description": "Interactive TypeScript quiz game",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"ts-node": "^10.9.0"
}
}
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Sample Output
========================================
TYPESCRIPT QUIZ GAME
========================================
Test your TypeScript knowledge!
Answer multiple choice questions and
see how well you score.
Select a category:
1. JavaScript Basics
2. TypeScript Types
3. Functions and Classes
4. Mixed (All Categories)
Your choice: 2
Select difficulty:
1. Easy
2. Medium
3. Hard
4. All Difficulties
Your choice: 2
========================================
Category: TypeScript Types
Difficulty: Medium
Questions: 5
========================================
Press Enter to continue...
----------------------------------------
Question 1/5
Difficulty: Medium
----------------------------------------
What is a union type?
1. A type that combines multiple types into one
2. A type that can be one of several specified types
3. A type for mathematical unions
4. A type for joining strings
Your answer (1-4): 2
*** CORRECT! ***
+10 points
Explanation: Union types allow a value to be one of several types, written as Type1 | Type2.
Progress: 1/1 correct | Score: 10 points
Press Enter to continue...
Extensions
1. Timed Questions
Add a time limit for each question:
interface TimedQuestion extends Question {
timeLimit: number; // seconds
}
// Reduce points for slow answers
function calculateTimedPoints(
difficulty: Difficulty,
timeTaken: number,
timeLimit: number
): number {
const basePoints = calculatePoints(difficulty);
const timeBonus = Math.max(0, (timeLimit - timeTaken) / timeLimit);
return Math.round(basePoints * (0.5 + 0.5 * timeBonus));
}
2. High Score Tracking
Save high scores to a file:
interface HighScore {
playerName: string;
score: number;
percentage: number;
category: string;
difficulty: string;
date: string;
}
// Save and load from highscores.json
3. More Question Types
Add different question types:
type QuestionType = 'multiple-choice' | 'true-false' | 'fill-blank';
interface TrueFalseQuestion {
type: 'true-false';
text: string;
correctAnswer: boolean;
}
4. Hints System
Allow players to use hints:
interface QuestionWithHints extends Question {
hints: string[];
hintsUsed: number;
}
// Reduce points when hints are used
Key Takeaways
- Literal types constrain values: Using
"easy" | "medium" | "hard"is safer than plain strings - Interfaces model complex data: Questions, results, and config are clearly defined
- Separate concerns into modules: Questions, scoring, UI, and engine are independent
- Type guards improve safety: Validating user input prevents runtime errors
- Generic shuffle function: The
shuffleArray<T>function works with any array type
Resources
| Resource | Type | Description |
|---|---|---|
| TypeScript Literal Types | Documentation | Literal type documentation |
| TypeScript Union Types | Documentation | Union type documentation |
| Array Methods | Documentation | JavaScript array methods |
Next Project
Ready for the final project? Continue to the Calculator with History.