TypeScript Tutorial: Building a Simple Web Application for a Pomodoro Timer

In the fast-paced world of software development, managing time effectively is crucial. The Pomodoro Technique, a time management method developed by Francesco Cirillo in the late 1980s, offers a simple yet powerful way to boost productivity. This technique involves working in focused 25-minute intervals, separated by short breaks. This tutorial will guide you through building a simple, yet functional, Pomodoro timer web application using TypeScript, a superset of JavaScript that adds static typing.

Why TypeScript?

TypeScript brings several advantages to the table, especially when building more complex applications. Here’s why it’s a great choice for this project:

  • Type Safety: TypeScript helps catch errors early in the development process by checking the types of your variables and function parameters. This reduces the likelihood of runtime errors.
  • Improved Code Readability: With type annotations, your code becomes easier to understand and maintain. It’s clear what data types are expected, making it simpler for you and others to work with the codebase.
  • Enhanced Developer Experience: TypeScript provides excellent tooling support, including autocompletion, refactoring, and error highlighting in most code editors.
  • Scalability: As your project grows, TypeScript’s type system helps you manage complexity and refactor your code more safely.

Project Setup

Before we dive into the code, let’s set up our project. You’ll need Node.js and npm (Node Package Manager) installed on your system. Open your terminal or command prompt and follow these steps:

  1. Create a Project Directory: Create a new directory for your project and navigate into it.
    mkdir pomodoro-timer-ts
     cd pomodoro-timer-ts
  2. Initialize npm: Initialize a new npm project.
    npm init -y
  3. Install TypeScript: Install TypeScript globally or locally.
    npm install --save-dev typescript
  4. Create a tsconfig.json file: This file configures the TypeScript compiler. You can generate a basic one with:
    npx tsc --init
  5. Install Parcel (Optional, but recommended): We’ll use Parcel to bundle our application. Install it as a development dependency.
    npm install --save-dev parcel

Your project structure should now look something like this:


pomodoro-timer-ts/
 |     ├── node_modules/
 |     ├── package.json
 |     ├── package-lock.json
 |     ├── tsconfig.json
 |     └── src/
 |        └── index.ts

Building the User Interface (HTML)

Let’s create a simple HTML structure for our Pomodoro timer. Create an index.html file in the root directory of your project (or in a public directory if you are using one). Here’s a basic example:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pomodoro Timer</title>
    <link rel="stylesheet" href="style.css"> <!-- Link to your CSS file -->
</head>
<body>
    <div class="container">
        <h1 id="timer-display">25:00</h1>
        <div class="controls">
            <button id="start-button">Start</button>
            <button id="stop-button">Stop</button>
            <button id="reset-button">Reset</button>
        </div>
    </div>
    <script src="index.ts"></script> <!-- Link to your TypeScript file -->
</body>
</html>

This HTML provides the basic structure: a display for the timer, and buttons to start, stop, and reset the timer. We’ve also added a link to a `style.css` file for styling. Create an empty `style.css` file in the same directory as your `index.html` file or in a public directory to add styling later.

Writing the TypeScript Code (Logic)

Now, let’s write the TypeScript code that will handle the timer logic. Create a file named index.ts inside a src directory (or another directory of your choice) in your project. This is where the magic happens.


// Define the initial timer values (in seconds)
const WORK_DURATION: number = 25 * 60; // 25 minutes
const BREAK_DURATION: number = 5 * 60;  // 5 minutes
let timeLeft: number = WORK_DURATION;
let timerInterval: NodeJS.Timeout | null = null;
let isRunning: boolean = false;
let isBreak: boolean = false;

// Get references to HTML elements
const timerDisplay = document.getElementById('timer-display') as HTMLHeadingElement | null;
const startButton = document.getElementById('start-button') as HTMLButtonElement | null;
const stopButton = document.getElementById('stop-button') as HTMLButtonElement | null;
const resetButton = document.getElementById('reset-button') as HTMLButtonElement | null;

// Function to format the time
const formatTime = (seconds: number): string => {
  const minutes: number = Math.floor(seconds / 60);
  const secondsLeft: number = seconds % 60;
  return `${minutes.toString().padStart(2, '0')}:${secondsLeft.toString().padStart(2, '0')}`;
};

// Function to update the timer display
const updateDisplay = () => {
  if (timerDisplay) {
    timerDisplay.textContent = formatTime(timeLeft);
  }
};

// Function to start the timer
const startTimer = () => {
  if (isRunning) return; // Prevent multiple timers from running
  isRunning = true;

  timerInterval = setInterval(() => {
    if (timeLeft > 0) {
      timeLeft--;
      updateDisplay();
    } else {
      clearInterval(timerInterval as NodeJS.Timeout);
      isRunning = false;
      if (isBreak) {
        timeLeft = WORK_DURATION; // Reset to work time
        isBreak = false;
        // Optionally, play a sound or show a notification that work time has started.
        // alert('Time to work!');
      } else {
        timeLeft = BREAK_DURATION; // Reset to break time
        isBreak = true;
        // Optionally, play a sound or show a notification that break time has started.
        // alert('Time for a break!');
      }
      updateDisplay();
      startTimer(); // Automatically start the next timer (work or break)
    }
  }, 1000);
};

// Function to stop the timer
const stopTimer = () => {
  if (!isRunning) return;
  clearInterval(timerInterval as NodeJS.Timeout);
  isRunning = false;
};

// Function to reset the timer
const resetTimer = () => {
  stopTimer();
  timeLeft = WORK_DURATION; // Reset to work time
  isBreak = false;
  updateDisplay();
};

// Event listeners for the buttons
if (startButton) {
  startButton.addEventListener('click', startTimer);
}

if (stopButton) {
  stopButton.addEventListener('click', stopTimer);
}

if (resetButton) {
  resetButton.addEventListener('click', resetTimer);
}

// Initial display update
updateDisplay();

Let’s break down this code:

  • Constants and Variables: We define constants for work and break durations and variables to track the time left, the timer interval, the running state, and whether we’re in a break.
  • HTML Element References: We get references to the HTML elements (timer display, start button, stop button, reset button) using their IDs. The `as HTMLHeadingElement | null` and `as HTMLButtonElement | null` type assertions help TypeScript understand the types of these elements, and the `| null` allows for null checks.
  • `formatTime` Function: This function takes the remaining time in seconds and formats it into a MM:SS string.
  • `updateDisplay` Function: This function updates the text content of the timer display with the formatted time.
  • `startTimer` Function: This is the core function. It starts the timer using `setInterval`. Inside the interval, it decrements the `timeLeft` every second, updates the display, and checks if the timer has reached zero. When the timer reaches zero, it switches between work and break durations and restarts the timer. It also includes checks to prevent multiple timers from running.
  • `stopTimer` Function: This function stops the timer using `clearInterval`.
  • `resetTimer` Function: This function resets the timer to the initial work duration.
  • Event Listeners: We attach event listeners to the start, stop, and reset buttons to call the respective functions when the buttons are clicked. We include null checks to avoid errors if the elements aren’t found.
  • Initial Display Update: We call `updateDisplay()` once at the end to initialize the timer display.

Styling with CSS

To make the timer look more appealing, let’s add some basic CSS. Open your style.css file and add the following:


body {
    font-family: sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    background-color: #f0f0f0;
}

.container {
    background-color: white;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    text-align: center;
}

#timer-display {
    font-size: 3em;
    margin-bottom: 20px;
}

.controls {
    display: flex;
    justify-content: center;
}

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

button:hover {
    background-color: #3e8e41;
}

Feel free to customize the styles to your liking. This CSS provides a basic layout and styling for the timer, display, and buttons.

Running the Application

Now, let’s run the application. Assuming you have Parcel installed, run the following command in your terminal:

npx parcel index.html

Parcel will bundle your HTML, TypeScript, and CSS files and start a development server. You should see a message in your terminal indicating the server’s address (usually `http://localhost:1234`). Open this address in your web browser, and you should see your Pomodoro timer! Click the “Start” button, and the timer should begin counting down. Test the “Stop” and “Reset” buttons as well.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to avoid or fix them:

  • Incorrect Element Selection: Make sure you are using the correct IDs when selecting HTML elements with `document.getElementById()`. Double-check your HTML to ensure the IDs match. Also, use type assertions (e.g., `as HTMLButtonElement | null`) to help TypeScript understand the types of the elements.
  • Uninitialized Variables: Ensure that all your variables are initialized before use. TypeScript will help you catch this.
  • Incorrect Time Formatting: The `formatTime` function is crucial. Ensure it correctly formats the time into MM:SS.
  • Missing Event Listeners: Make sure your event listeners are correctly attached to the buttons. If the buttons aren’t working, check if the event listeners are correctly assigned.
  • Timer Not Stopping: If the timer doesn’t stop, double-check your `stopTimer` function and the logic within the `startTimer` function to ensure `clearInterval` is called correctly.
  • Multiple Timers Running: The `isRunning` flag in the `startTimer` function prevents multiple timers from running concurrently.
  • Type Errors: TypeScript will highlight type errors in your code. Pay attention to these errors and fix them by ensuring your variables and function parameters have the correct types.

Key Takeaways

Here’s a recap of the key concepts covered in this tutorial:

  • TypeScript Fundamentals: We used TypeScript to write type-safe code, improving code readability and maintainability.
  • HTML Structure: We created a basic HTML structure for the Pomodoro timer, including the display and control buttons.
  • Timer Logic: We implemented the core timer logic using `setInterval`, `clearInterval`, and functions to format and update the time.
  • Event Handling: We used event listeners to handle button clicks and trigger the timer’s start, stop, and reset actions.
  • CSS Styling: We added basic CSS to style the timer and make it visually appealing.
  • Parcel Bundling: We used Parcel to bundle our application for easy development and deployment.

FAQ

  1. Can I use this timer for actual work? Yes, absolutely! This is a functional Pomodoro timer that you can use to manage your time and increase productivity. You might want to enhance it with features like sound notifications or a more sophisticated user interface.
  2. How can I add sound notifications? You can add sound notifications by using the `<audio>` HTML element and the `play()` method in your JavaScript code. Trigger the sound when the timer reaches zero or when the break starts/ends.
  3. How can I customize the work and break durations? You can add input fields to allow the user to set the work and break durations dynamically. Update the `WORK_DURATION` and `BREAK_DURATION` constants based on the user’s input.
  4. How can I deploy this application? You can deploy this application using various platforms like Netlify, Vercel, or GitHub Pages. These platforms typically require you to build your project (using Parcel, for example) and then upload the build files.
  5. What are some advanced features I can add? You could add features like a task list, session tracking, and settings to customize the timer’s behavior.

You’ve now successfully built a functional Pomodoro timer using TypeScript. This project showcases the power of TypeScript for building web applications and provides a solid foundation for further exploration. The principles of time management, combined with the clarity and robustness of TypeScript, create a potent combination for productivity. With this foundation, you can now extend the application with additional features, experiment with different styling options, and even integrate it into your daily workflow. Remember to practice the Pomodoro technique itself as you work on this and other projects to maximize your focus and efficiency.