TypeScript: Building a Simple Interactive Pomodoro Timer

In the fast-paced world of software development, time management is critical. Developers often juggle multiple tasks, and staying focused can be a challenge. The Pomodoro Technique offers a simple yet effective method to enhance productivity by breaking work into focused intervals, typically 25 minutes, separated by short breaks. In this tutorial, we will build a fully functional Pomodoro Timer using TypeScript, a superset of JavaScript that adds static typing, making your code more maintainable and less prone to errors. This project is ideal for beginners and intermediate developers looking to deepen their understanding of TypeScript while creating a practical, useful application.

Why Build a Pomodoro Timer?

Creating a Pomodoro Timer provides several benefits:

  • Practical Application: You gain a tangible tool for improving focus and productivity.
  • TypeScript Fundamentals: You’ll reinforce your understanding of core concepts like variables, functions, types, and event handling.
  • UI Interaction: You’ll learn how to manipulate the Document Object Model (DOM) to update the user interface dynamically.
  • Code Organization: You’ll practice structuring your code for readability and maintainability.

By the end of this tutorial, you’ll have a working Pomodoro Timer, a solid grasp of TypeScript fundamentals, and the skills to tackle more complex web development projects.

Setting Up Your Development Environment

Before we start coding, let’s set up our development environment. You’ll need:

  • Node.js and npm (Node Package Manager): Used to manage project dependencies and run TypeScript code. Download and install from https://nodejs.org/.
  • A Code Editor: Such as Visual Studio Code (VS Code), Atom, or Sublime Text. VS Code is highly recommended due to its excellent TypeScript support.
  • A Terminal: To run commands and compile your TypeScript code.

Once you have Node.js installed, open your terminal and navigate to your desired project directory. Then, create a new project directory (e.g., `pomodoro-timer`) and initialize a new npm project using the following command:

npm init -y

This command creates a `package.json` file, which will manage your project’s dependencies.

Installing TypeScript

Next, install TypeScript as a development dependency:

npm install --save-dev typescript

This command installs the TypeScript compiler (`tsc`).

Configuring TypeScript

To configure TypeScript, create a `tsconfig.json` file in your project’s root directory. You can generate a basic `tsconfig.json` file using the following command:

npx tsc --init

This command creates a `tsconfig.json` file with default settings. You can customize these settings to suit your project’s needs. For our Pomodoro Timer, let’s modify the following settings:

{
  "compilerOptions": {
    "target": "es5",       // Compile to ES5 JavaScript
    "module": "commonjs",  // Module system
    "outDir": "./dist",      // Output directory for compiled JavaScript
    "strict": true,        // Enable strict type checking
    "esModuleInterop": true, // Enables interoperability between CommonJS and ES Modules
    "skipLibCheck": true   // Skip type checking of declaration files
  },
  "include": ["src/**/*"] // Include all TypeScript files in the src directory
}

These settings configure the TypeScript compiler to:

  • Compile to ES5 JavaScript (for broader browser compatibility).
  • Use the CommonJS module system.
  • Output compiled JavaScript files to a `dist` directory.
  • Enable strict type checking.
  • Enable interoperability between CommonJS and ES Modules.
  • Include all TypeScript files in the `src` directory.

Project Structure

Let’s establish a basic project structure:

pomodoro-timer/
├── package.json
├── tsconfig.json
├── src/
│   ├── index.ts        // Main TypeScript file
│   └── styles.css      // CSS file for styling
├── dist/
│   └── index.js        // Compiled JavaScript file
└── index.html          // HTML file

Create these files and directories in your project. We’ll write the code for each of these files in the following sections.

Writing the HTML (index.html)

Create an `index.html` file with the following content. This HTML provides the basic structure for our Pomodoro Timer, including the timer display, control buttons, and a visual representation of the timer’s progress.

<!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="styles.css">
</head>
<body>
    <div class="container">
        <h1>Pomodoro Timer</h1>
        <div class="timer">25:00</div>
        <div class="controls">
            <button id="startStop">Start</button>
            <button id="reset">Reset</button>
        </div>
        <div class="progress-bar-container">
            <div class="progress-bar"></div>
        </div>
    </div>
    <script src="dist/index.js"></script>
</body>
</html>

This HTML sets up the basic layout:

  • A container div (`.container`) to hold all elements.
  • A heading (`<h1>`) for the title.
  • A div (`.timer`) to display the timer.
  • A div (`.controls`) with “Start/Stop” and “Reset” buttons.
  • A div (`.progress-bar-container`) and a child div (`.progress-bar`) to visually represent the timer’s progress.
  • A link to the `styles.css` file for styling.
  • A link to the compiled JavaScript file (`dist/index.js`).

Styling with CSS (styles.css)

Create a `styles.css` file to style the Pomodoro Timer. Add the following CSS code:

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: 10px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    text-align: center;
}

.timer {
    font-size: 3em;
    margin: 20px 0;
}

.controls button {
    padding: 10px 20px;
    margin: 0 10px;
    border: none;
    border-radius: 5px;
    background-color: #4CAF50; /* Green */
    color: white;
    cursor: pointer;
    font-size: 1em;
}

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

.progress-bar-container {
    width: 100%;
    height: 10px;
    background-color: #eee;
    border-radius: 5px;
    margin-top: 20px;
}

.progress-bar {
    height: 100%;
    width: 0%;
    background-color: #4CAF50;
    border-radius: 5px;
}

This CSS provides basic styling for the timer’s appearance, including font, layout, button styles, and the progress bar.

Writing the TypeScript Code (index.ts)

Now, let’s write the TypeScript code for the Pomodoro Timer. This is the core of our application, where we will define the timer logic, handle user interactions, and update the UI.


// Define the different timer states
const enum TimerState {
    Stopped,   // Timer is not running
    Running,   // Timer is counting down
    Paused     // Timer is paused
}

// Get references to HTML elements
const timerDisplay = document.querySelector('.timer') as HTMLDivElement;
const startStopButton = document.getElementById('startStop') as HTMLButtonElement;
const resetButton = document.getElementById('reset') as HTMLButtonElement;
const progressBar = document.querySelector('.progress-bar') as HTMLDivElement;

// Constants for the timer duration (in seconds)
const WORK_DURATION = 25 * 60; // 25 minutes
const BREAK_DURATION = 5 * 60; // 5 minutes

// State variables
let timerState: TimerState = TimerState.Stopped; // Initial state: stopped
let timeLeft: number = WORK_DURATION; // Initial time left: work duration
let intervalId: number | null = null; // Store the interval ID for clearing
let isBreak: boolean = false; // Flag to indicate if it's a break

// Function to format time (seconds to mm:ss)
const formatTime = (seconds: number): string => {
    const minutes = Math.floor(seconds / 60);
    const remainingSeconds = seconds % 60;
    const formattedMinutes = String(minutes).padStart(2, '0');
    const formattedSeconds = String(remainingSeconds).padStart(2, '0');
    return `${formattedMinutes}:${formattedSeconds}`;
};

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

// Function to update the progress bar
const updateProgressBar = (): void => {
    let duration = isBreak ? BREAK_DURATION : WORK_DURATION;
    let progress = (1 - (timeLeft / duration)) * 100;
    if (progressBar) {
        progressBar.style.width = `${progress}%`;
    }
};

// Function to start the timer
const startTimer = (): void => {
    if (timerState === TimerState.Stopped || timerState === TimerState.Paused) {
        timerState = TimerState.Running;
        startStopButton.textContent = 'Pause';
        intervalId = setInterval(() => {
            if (timeLeft > 0) {
                timeLeft--;
                updateTimerDisplay();
                updateProgressBar();
            } else {
                clearInterval(intervalId as number);
                // Handle timer end (switch to break or work)
                if (!isBreak) {
                    isBreak = true;
                    timeLeft = BREAK_DURATION;
                    alert('Time for a break!');
                } else {
                    isBreak = false;
                    timeLeft = WORK_DURATION;
                    alert('Back to work!');
                }
                updateTimerDisplay();
                updateProgressBar();
                startTimer(); // Automatically start the next cycle
            }
        }, 1000); // Update every second
    }
};

// Function to pause the timer
const pauseTimer = (): void => {
    if (timerState === TimerState.Running) {
        timerState = TimerState.Paused;
        startStopButton.textContent = 'Resume';
        if (intervalId) {
            clearInterval(intervalId);
        }
    }
};

// Function to stop and reset the timer
const resetTimer = (): void => {
    timerState = TimerState.Stopped;
    isBreak = false;
    timeLeft = WORK_DURATION;
    startStopButton.textContent = 'Start';
    if (intervalId) {
        clearInterval(intervalId);
    }
    updateTimerDisplay();
    updateProgressBar();
};

// Event listeners for the buttons
if (startStopButton) {
    startStopButton.addEventListener('click', () => {
        if (timerState === TimerState.Stopped || timerState === TimerState.Paused) {
            startTimer();
        } else {
            pauseTimer();
        }
    });
}

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

// Initial display update
updateTimerDisplay();

Let’s break down the TypeScript code:

  • Type Definitions: We define a `TimerState` enum to manage the different states of the timer (Stopped, Running, Paused).
  • DOM Element References: We use `document.querySelector` and `document.getElementById` to get references to the HTML elements we need to interact with (timer display, buttons, progress bar). We use type assertions (e.g., `as HTMLDivElement`) to tell TypeScript the type of these elements.
  • Constants: We define constants for the work and break durations.
  • State Variables: We declare variables to store the current timer state, time left, the interval ID, and a flag indicating whether it’s a break.
  • `formatTime()` Function: This function takes the time in seconds and formats it into a `mm:ss` string.
  • `updateTimerDisplay()` Function: This function updates the timer display in the HTML with the formatted time.
  • `updateProgressBar()` Function: This function calculates the progress percentage and updates the progress bar’s width.
  • `startTimer()` Function: This function starts the timer. It sets the `timerState` to `Running`, changes the button text to “Pause”, and uses `setInterval` to decrement the `timeLeft` every second. When the timer reaches 0, it switches to the break duration (or back to work), updates the display, and restarts the timer.
  • `pauseTimer()` Function: This function pauses the timer. It sets the `timerState` to `Paused`, changes the button text to “Resume”, and clears the interval using `clearInterval`.
  • `resetTimer()` Function: This function resets the timer to its initial state (stopped, work duration).
  • Event Listeners: We add event listeners to the “Start/Stop” and “Reset” buttons to handle user interactions.
  • Initial Display Update: We call `updateTimerDisplay()` to set the initial time on the display.

Compiling and Running the Application

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

tsc

This command will compile the `index.ts` file and create a `index.js` file in the `dist` directory. If you encounter errors during compilation, carefully review the error messages and fix any type errors or syntax issues in your TypeScript code.

To run the application, open the `index.html` file in your web browser. You should see the Pomodoro Timer with the timer display, start/stop and reset buttons, and a progress bar. Click the “Start” button to start the timer. The timer will count down, and the progress bar will update. When the timer reaches zero, you’ll receive an alert, and the timer will switch to the break duration (or back to work).

Common Mistakes and Troubleshooting

Here are some common mistakes and how to fix them:

  • Incorrect File Paths: Double-check that the file paths in your `index.html` and `tsconfig.json` are correct. For example, ensure that the path to your compiled JavaScript file in the `<script src=”dist/index.js”>` tag is correct.
  • Type Errors: TypeScript’s strict type checking can catch errors early. Carefully review any type errors reported by the compiler and fix them by ensuring your variables and function parameters have the correct types.
  • Missing DOM Element References: Make sure your HTML elements have the correct IDs and classes, and that your TypeScript code correctly selects those elements using `document.querySelector` and `document.getElementById`. If you get an error that says “Cannot read properties of null (reading ‘textContent’)”, it means that your JavaScript can’t find the HTML element.
  • Incorrect Time Calculations: If the timer is not counting down correctly, double-check your time calculations, especially the `timeLeft–` line and the logic for handling breaks.
  • Unclear Error Messages: If you encounter errors, carefully read the error messages in the browser’s developer console (usually opened by pressing F12) and in the terminal where you run `tsc`. These messages provide valuable information about the problem.

Key Takeaways

  • TypeScript Fundamentals: You’ve used variables, functions, types, enums, event listeners, and DOM manipulation.
  • Project Structure: You’ve learned how to organize your code into files and directories.
  • Practical Application: You’ve built a functional Pomodoro Timer that you can use daily.
  • Debugging: You’ve gained experience in troubleshooting and fixing common errors.

FAQ

  1. How do I change the work and break durations?

    Modify the `WORK_DURATION` and `BREAK_DURATION` constants in your `index.ts` file. Remember that these durations are in seconds.

  2. How can I add sounds when the timer ends?

    You can add an audio element in your HTML and use JavaScript to play the sound when the timer reaches zero. You would need to add an `<audio>` tag in your HTML and then use JavaScript’s `new Audio()` and `play()` methods.

  3. How can I make the timer persistent (save the time even if the page is refreshed)?

    You can use local storage to save the timer’s state (time left, isBreak, timerState) in the user’s browser. When the page loads, retrieve the saved state from local storage. Use `localStorage.setItem()` to save data and `localStorage.getItem()` to retrieve it.

  4. How can I add a settings menu to customize the work and break intervals?

    You can create a modal or a separate section in your HTML with input fields for users to set custom work and break durations. You would then need to read the values from these input fields in your JavaScript and update the `WORK_DURATION` and `BREAK_DURATION` constants, and then restart the timer. Use event listeners on input fields to detect changes.

  5. How do I deploy this application?

    You can deploy this application by using a service like Netlify or Vercel. You will need to push your code to a Git repository (like GitHub). Then, configure your deployment service to build your project (using `npm install` and `tsc` commands) and serve the `index.html` file.

Creating this Pomodoro Timer is just the beginning. The principles and techniques you’ve learned can be applied to many other web development projects. Consider expanding this project with features like custom timer settings, sound notifications, and a more advanced user interface. You could also explore different JavaScript frameworks like React, Vue, or Angular to build more complex and interactive web applications. As you continue to practice and learn, you will become more proficient in TypeScript and web development, leading to exciting opportunities in the field. Embrace the challenges, experiment with new ideas, and never stop learning. The world of web development is constantly evolving, and there is always something new to discover. Keep coding, keep building, and keep growing.