TypeScript Tutorial: Build a Simple Web-Based Interactive Game

In the world of web development, creating interactive and engaging user experiences is key. Games, with their inherent interactivity, are a fantastic way to learn and practice fundamental programming concepts. This tutorial will guide you through building a simple, yet engaging, web-based interactive game using TypeScript. We’ll cover everything from setting up your project to implementing game logic, all while focusing on clear explanations and practical examples.

Why TypeScript?

TypeScript, a superset of JavaScript, adds static typing to the language. This means you can catch potential errors during development, before your code runs in a browser. TypeScript offers several benefits:

  • Improved Code Quality: Static typing helps prevent runtime errors.
  • Enhanced Readability: Types make your code easier to understand.
  • Better Tooling: IDEs provide better autocompletion and refactoring.
  • Scalability: TypeScript projects are easier to maintain as they grow.

For this project, TypeScript will help us write cleaner, more maintainable code, and catch errors early on, making the development process smoother.

Project Setup

Let’s get started by setting up our project. We’ll use npm (Node Package Manager) to manage our dependencies and TypeScript to compile our code.

1. Initialize the Project

Open your terminal or command prompt and navigate to your desired project directory. Then, run the following command to initialize a new npm project:

npm init -y

This command creates a package.json file, which will store your project’s metadata and dependencies.

2. Install TypeScript

Next, install TypeScript as a development dependency:

npm install --save-dev typescript

3. Configure TypeScript

Create a tsconfig.json file in your project’s root directory. This file configures the TypeScript compiler. You can generate a basic tsconfig.json file using the following command:

npx tsc --init

This will create a tsconfig.json file with default settings. You can customize these settings to fit your project’s needs. For our project, we’ll keep the default settings for simplicity. A typical tsconfig.json file might look like this:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

Here’s a brief explanation of some of the key options:

  • target: Specifies the JavaScript version to compile to.
  • module: Specifies the module system to use (e.g., CommonJS, ESNext).
  • outDir: Specifies the output directory for the compiled JavaScript files.
  • rootDir: Specifies the root directory of your TypeScript source files.
  • strict: Enables strict type-checking options.
  • esModuleInterop: Enables interoperability between CommonJS and ES modules.
  • skipLibCheck: Skips type-checking of declaration files.
  • forceConsistentCasingInFileNames: Enforces consistent casing in filenames.
  • include: Specifies the files or patterns to include in compilation.

4. Create Project Structure

Create a src directory in your project’s root directory. This is where we’ll put our TypeScript source files. Inside the src directory, create an index.ts file. This will be the entry point of our game.

Building the Game: A Simple Guessing Game

For this tutorial, we’ll build a number guessing game. The game will generate a random number, and the player will have to guess it. We’ll provide feedback on whether the guess is too high or too low.

1. Generate a Random Number

Let’s start by generating a random number. Open src/index.ts and add the following code:

// Generate a random number between 1 and 100
const randomNumber = Math.floor(Math.random() * 100) + 1;

console.log("The secret number is:", randomNumber); // For debugging purposes

In this code:

  • Math.random() generates a random floating-point number between 0 (inclusive) and 1 (exclusive).
  • We multiply it by 100 to get a number between 0 and 99.
  • Math.floor() rounds the number down to the nearest integer.
  • We add 1 to get a number between 1 and 100.

2. Get Player Input

Next, we need to get the player’s guess. We’ll use the prompt() function to get input from the player. However, note that prompt() is a browser-specific function and won’t work in a Node.js environment without a workaround. We will use it here for simplicity, assuming a browser environment.


const guess = prompt("Guess a number between 1 and 100:");

This will display a prompt box asking the player to enter their guess. The input will be stored in the guess variable as a string.

3. Validate the Input

The player’s input is a string, but we need to compare it with a number. We need to convert the input to a number and also handle potential errors, such as the player entering non-numeric input.


let playerGuess: number | null = null;

if (guess !== null) {
  const parsedGuess = parseInt(guess, 10);
  if (!isNaN(parsedGuess)) {
    playerGuess = parsedGuess;
  } else {
    alert("Invalid input. Please enter a number.");
  }
}

Here’s what the code does:

  • We initialize playerGuess to null.
  • We check if the guess is not null (the player didn’t cancel the prompt).
  • parseInt(guess, 10) attempts to convert the string to an integer (base 10).
  • isNaN(parsedGuess) checks if the result is not a number. If it’s not a number, we display an error message.

4. Compare the Guess

Now, let’s compare the player’s guess with the random number and provide feedback.


if (playerGuess !== null) {
  if (playerGuess === randomNumber) {
    alert("Congratulations! You guessed the number!");
  } else if (playerGuess < randomNumber) {
    alert("Too low! Try again.");
  } else {
    alert("Too high! Try again.");
  }
}

This code checks the following:

  • If playerGuess is not null (a valid number was entered):
  • If the guess is correct, we display a congratulatory message.
  • If the guess is too low, we display a “Too low!” message.
  • If the guess is too high, we display a “Too high!” message.

5. Putting it all together

Here’s the complete code for src/index.ts:


// Generate a random number between 1 and 100
const randomNumber = Math.floor(Math.random() * 100) + 1;

console.log("The secret number is:", randomNumber); // For debugging purposes

const guess = prompt("Guess a number between 1 and 100:");

let playerGuess: number | null = null;

if (guess !== null) {
  const parsedGuess = parseInt(guess, 10);
  if (!isNaN(parsedGuess)) {
    playerGuess = parsedGuess;
  } else {
    alert("Invalid input. Please enter a number.");
  }
}

if (playerGuess !== null) {
  if (playerGuess === randomNumber) {
    alert("Congratulations! You guessed the number!");
  } else if (playerGuess < randomNumber) {
    alert("Too low! Try again.");
  } else {
    alert("Too high! Try again.");
  }
}

6. Compile and Run

To compile your TypeScript code, run the following command in your terminal:

tsc

This will generate a dist directory containing the compiled JavaScript file (index.js). You can then open an HTML file in your browser and include this JavaScript file. For instance:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Guessing Game</title>
</head>
<body>
    <script src="dist/index.js"></script>
</body>
</html>

Open this HTML file in your browser. You should see a prompt asking you to guess a number. Enter your guess and interact with the game.

Enhancing the Game

The basic guessing game is functional, but we can enhance it to improve the user experience and add more features.

1. Adding Game Loops and Multiple Guesses

Currently, the game only allows one guess. Let’s add a loop to allow multiple guesses until the player guesses correctly or runs out of attempts.


const randomNumber = Math.floor(Math.random() * 100) + 1;
let attempts = 5;

console.log("The secret number is:", randomNumber); // For debugging purposes

while (attempts > 0) {
  const guess = prompt(`Guess a number between 1 and 100. Attempts remaining: ${attempts}:`);
  let playerGuess: number | null = null;

  if (guess !== null) {
    const parsedGuess = parseInt(guess, 10);
    if (!isNaN(parsedGuess)) {
      playerGuess = parsedGuess;
    } else {
      alert("Invalid input. Please enter a number.");
      continue; // Skip to the next iteration of the loop
    }
  }

  if (playerGuess !== null) {
    if (playerGuess === randomNumber) {
      alert(`Congratulations! You guessed the number in ${6 - attempts} attempts!`);
      break; // Exit the loop
    } else if (playerGuess < randomNumber) {
      alert("Too low! Try again.");
    } else {
      alert("Too high! Try again.");
    }
    attempts--;
  }
}

if (attempts === 0) {
  alert(`You ran out of attempts. The number was ${randomNumber}.`);
}

Key changes:

  • We added a attempts variable to track the number of guesses remaining.
  • We use a while loop to allow multiple guesses.
  • The prompt now displays the number of attempts remaining.
  • We added a continue statement to skip to the next iteration if the input is invalid.
  • We added a break statement to exit the loop if the player guesses correctly.
  • We added a message if the player runs out of attempts.

2. Implementing a Score

Let’s add a scoring system to make the game more engaging. The score can be based on the number of attempts taken to guess the number.


const randomNumber = Math.floor(Math.random() * 100) + 1;
let attempts = 5;
let score = 0;

console.log("The secret number is:", randomNumber); // For debugging purposes

while (attempts > 0) {
  const guess = prompt(`Guess a number between 1 and 100. Attempts remaining: ${attempts}`);
  let playerGuess: number | null = null;

  if (guess !== null) {
    const parsedGuess = parseInt(guess, 10);
    if (!isNaN(parsedGuess)) {
      playerGuess = parsedGuess;
    } else {
      alert("Invalid input. Please enter a number.");
      continue;
    }
  }

  if (playerGuess !== null) {
    if (playerGuess === randomNumber) {
      score = attempts * 10; // Award points based on attempts left
      alert(`Congratulations! You guessed the number in ${6 - attempts} attempts! Your score is ${score}.`);
      break;
    } else if (playerGuess < randomNumber) {
      alert("Too low! Try again.");
    } else {
      alert("Too high! Try again.");
    }
    attempts--;
  }
}

if (attempts === 0) {
  alert(`You ran out of attempts. The number was ${randomNumber}.`);
}

We’ve added a score variable and updated it when the player guesses correctly. The score is calculated based on the number of attempts remaining.

3. Adding Difficulty Levels

Let’s add difficulty levels to make the game more challenging and customizable. We can adjust the range of numbers and the number of attempts based on the chosen difficulty.


// Define difficulty levels
const difficultyLevels = {
  easy: { range: 10, attempts: 7 },
  medium: { range: 50, attempts: 5 },
  hard: { range: 100, attempts: 3 },
};

// Get difficulty level from the player
const difficulty = prompt("Choose difficulty: easy, medium, hard")?.toLowerCase() || "medium";

// Get difficulty settings
const selectedDifficulty = difficultyLevels[difficulty as keyof typeof difficultyLevels] || difficultyLevels.medium;

const randomNumber = Math.floor(Math.random() * selectedDifficulty.range) + 1;
let attempts = selectedDifficulty.attempts;

console.log("The secret number is:", randomNumber); // For debugging purposes

while (attempts > 0) {
  const guess = prompt(`Guess a number between 1 and ${selectedDifficulty.range}. Attempts remaining: ${attempts}`);
  let playerGuess: number | null = null;

  if (guess !== null) {
    const parsedGuess = parseInt(guess, 10);
    if (!isNaN(parsedGuess)) {
      playerGuess = parsedGuess;
    } else {
      alert("Invalid input. Please enter a number.");
      continue;
    }
  }

  if (playerGuess !== null) {
    if (playerGuess === randomNumber) {
      const score = attempts * 10;
      alert(`Congratulations! You guessed the number in ${selectedDifficulty.attempts - attempts + 1} attempts! Your score is ${score}.`);
      break;
    } else if (playerGuess < randomNumber) {
      alert("Too low! Try again.");
    } else {
      alert("Too high! Try again.");
    }
    attempts--;
  }
}

if (attempts === 0) {
  alert(`You ran out of attempts. The number was ${randomNumber}.`);
}

Here’s how we implemented difficulty levels:

  • We defined an object difficultyLevels to store the settings for each difficulty.
  • We prompt the player to choose a difficulty level.
  • We retrieve the corresponding settings based on the player’s choice.
  • We use the selected settings to generate the random number, set the number of attempts, and display the game instructions.

4. Using HTML and CSS for a Better UI

While the prompt() and alert() functions are simple, they provide a basic user experience. For a more engaging game, we can use HTML, CSS, and JavaScript to build a richer user interface.

Here’s a basic HTML structure for our game:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Guessing Game</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>Number Guessing Game</h1>
        <p>Guess a number between 1 and <span id="range">100</span>.</p>
        <input type="number" id="guessInput">
        <button id="guessButton">Guess</button>
        <p id="feedback"></p>
        <p id="attemptsLeft"></p>
        <p id="score"></p>
    </div>
    <script src="dist/index.js"></script>
</body>
</html>

And here’s some basic CSS in style.css:

.container {
    width: 300px;
    margin: 50px auto;
    padding: 20px;
    border: 1px solid #ccc;
    border-radius: 5px;
    text-align: center;
}

input[type="number"] {
    width: 100%;
    padding: 10px;
    margin: 10px 0;
    border: 1px solid #ccc;
    border-radius: 4px;
    box-sizing: border-box;
}

button {
    background-color: #4CAF50;
    color: white;
    padding: 10px 20px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

Now, let’s modify the TypeScript code to interact with the HTML elements.


// Define difficulty levels
const difficultyLevels = {
    easy: { range: 10, attempts: 7 },
    medium: { range: 50, attempts: 5 },
    hard: { range: 100, attempts: 3 },
};

// Get difficulty level from the player
const difficulty = prompt("Choose difficulty: easy, medium, hard")?.toLowerCase() || "medium";

// Get difficulty settings
const selectedDifficulty = difficultyLevels[difficulty as keyof typeof difficultyLevels] || difficultyLevels.medium;

const randomNumber = Math.floor(Math.random() * selectedDifficulty.range) + 1;
let attempts = selectedDifficulty.attempts;
let score = 0;

// Get HTML elements
const guessInput = document.getElementById('guessInput') as HTMLInputElement;
const guessButton = document.getElementById('guessButton') as HTMLButtonElement;
const feedback = document.getElementById('feedback') as HTMLElement;
const attemptsLeft = document.getElementById('attemptsLeft') as HTMLElement;
const scoreDisplay = document.getElementById('score') as HTMLElement;
const rangeDisplay = document.getElementById('range') as HTMLElement;

// Set the range in the UI
if (rangeDisplay) {
    rangeDisplay.textContent = String(selectedDifficulty.range);
}

// Update attempts left display
function updateAttemptsLeft() {
    if (attemptsLeft) {
        attemptsLeft.textContent = `Attempts left: ${attempts}`;
    }
}

updateAttemptsLeft();

// Handle guess button click
guessButton?.addEventListener('click', () => {
    const guess = guessInput.value;
    if (!guess) {
        feedback.textContent = 'Please enter a number.';
        return;
    }

    const playerGuess = parseInt(guess, 10);
    if (isNaN(playerGuess)) {
        feedback.textContent = 'Invalid input. Please enter a number.';
        return;
    }

    if (playerGuess  selectedDifficulty.range) {
        feedback.textContent = `Please enter a number between 1 and ${selectedDifficulty.range}.`;
        return;
    }

    if (playerGuess === randomNumber) {
        score = attempts * 10;
        feedback.textContent = `Congratulations! You guessed the number in ${selectedDifficulty.attempts - attempts + 1} attempts!`;
        scoreDisplay.textContent = `Your score: ${score}`;
        guessButton.disabled = true;
    } else if (playerGuess < randomNumber) {
        feedback.textContent = 'Too low! Try again.';
    } else {
        feedback.textContent = 'Too high! Try again.';
    }

    attempts--;
    updateAttemptsLeft();

    if (attempts === 0) {
        feedback.textContent = `You ran out of attempts. The number was ${randomNumber}.`;
        guessButton.disabled = true;
    }
});

Key changes:

  • We get references to the HTML elements using document.getElementById().
  • We add an event listener to the guess button.
  • Inside the event listener, we get the player’s guess from the input field.
  • We update the feedback and attempts left in the HTML.
  • We disable the guess button when the game is over.

This approach provides a more interactive and user-friendly experience.

Common Mistakes and How to Fix Them

When working with TypeScript and game development, it’s easy to make mistakes. Here are some common pitfalls and how to avoid them:

1. Incorrect Type Annotations

Mistake: Forgetting to specify the type of variables or using the wrong types.

Fix: Always use type annotations (: type) to specify the type of variables, function parameters, and return values. This helps catch errors early and improves code readability.

// Incorrect: No type annotation
let score = 0;

// Correct: Type annotation
let score: number = 0;

2. Null or Undefined Errors

Mistake: Not handling potential null or undefined values, leading to runtime errors.

Fix: Use the non-null assertion operator (!) cautiously, or use the optional chaining operator (?.) and nullish coalescing operator (??) to safely access properties and methods. Consider using type unions (number | null) to allow for null values.

// Incorrect: Potential error if element is null
const element = document.getElementById('myElement');
element.textContent = 'Hello'; // Error: element can be null

// Correct: Using optional chaining
const element = document.getElementById('myElement');
element?.textContent = 'Hello'; // Safe: does nothing if element is null

// Correct: Using nullish coalescing
const value = someVariable ?? defaultValue; // Assigns defaultValue if someVariable is null or undefined

3. Incorrect DOM Manipulation

Mistake: Trying to access or modify HTML elements before they are loaded.

Fix: Ensure that your JavaScript code runs after the HTML elements have loaded. You can do this by placing your <script> tag at the end of the <body> or by using the DOMContentLoaded event.

document.addEventListener('DOMContentLoaded', () => {
  // Your code here
  const element = document.getElementById('myElement');
  if (element) {
    element.textContent = 'Hello';
  }
});

4. Scope Issues

Mistake: Not understanding the scope of variables and functions, leading to unexpected behavior.

Fix: Be mindful of variable scope (global, function, block). Use let and const to declare variables to limit their scope. Use closures to create private variables and functions.

5. Ignoring Error Messages

Mistake: Ignoring the error messages from the TypeScript compiler.

Fix: Pay close attention to the error messages provided by the TypeScript compiler. They often provide valuable information about the problem and how to fix it. Use these messages as your primary debugging tool.

Key Takeaways

  • TypeScript adds static typing to JavaScript, improving code quality and maintainability.
  • Setting up a TypeScript project involves initializing npm, installing TypeScript, configuring tsconfig.json, and creating a project structure.
  • Building a game involves generating random numbers, getting player input, validating the input, and comparing the guess.
  • Enhancing the game includes adding game loops, implementing a scoring system, adding difficulty levels, and using HTML/CSS for a better UI.
  • Common mistakes include incorrect type annotations, not handling null/undefined values, incorrect DOM manipulation, scope issues, and ignoring error messages.

Frequently Asked Questions (FAQ)

1. How do I compile TypeScript code?

You can compile TypeScript code using the tsc command in your terminal. This command reads your tsconfig.json file and compiles your TypeScript files into JavaScript files.

2. How do I debug TypeScript code?

You can debug TypeScript code using your browser’s developer tools or a debugger in your IDE. Make sure your source maps are enabled in your tsconfig.json file ("sourceMap": true) to map the compiled JavaScript code back to your TypeScript source code.

3. What are the benefits of using TypeScript for game development?

TypeScript offers several benefits for game development, including improved code quality, enhanced readability, better tooling, and scalability. Static typing helps catch errors early, making the development process smoother and reducing the likelihood of runtime errors.

4. How can I add more features to my game?

You can add more features to your game by implementing features such as:

  • More interactive elements
  • More complex game logic
  • Different difficulty levels
  • More advanced scoring systems
  • Visual enhancements with CSS and JavaScript

5. Where can I learn more about TypeScript?

There are many resources available to learn more about TypeScript, including the official TypeScript documentation, online tutorials, and courses. You can also explore open-source TypeScript projects on GitHub to learn from other developers.

Building an interactive game in TypeScript is a rewarding learning experience. By following this tutorial, you’ve gained a solid foundation in setting up a TypeScript project, implementing game logic, and creating a user-friendly interface. Remember that the journey of a thousand lines of code begins with a single step. Experiment with different features, explore advanced concepts, and don’t be afraid to make mistakes – that’s how you learn and grow as a developer. Keep coding, keep experimenting, and most importantly, have fun creating!