TypeScript Tutorial: Building a Simple Interactive To-Do List App

Are you tired of juggling multiple apps, sticky notes, and mental checklists to manage your daily tasks? In today’s fast-paced world, staying organized is more crucial than ever. A well-designed to-do list application can be a game-changer, helping you prioritize, track progress, and ultimately, be more productive. This tutorial will guide you through building a simple, yet functional, interactive to-do list app using TypeScript, a powerful superset of JavaScript. We’ll cover everything from setting up your development environment to adding features like adding, deleting, and marking tasks as complete. By the end of this tutorial, you’ll not only have a practical tool but also a solid understanding of TypeScript fundamentals and how they can be applied to real-world projects.

Why TypeScript?

Before we dive in, let’s address the elephant in the room: Why TypeScript? While JavaScript is the language of the web, TypeScript offers several advantages, especially for larger projects:

  • Type Safety: TypeScript introduces static typing, which means you can define the types of variables, function parameters, and return values. This helps catch errors early in the development process, reducing debugging time and making your code more robust.
  • Improved Code Maintainability: With types, your code becomes more self-documenting, making it easier to understand and maintain, especially when working on a team or revisiting a project after a long time.
  • Enhanced Developer Experience: TypeScript provides better autocompletion, refactoring tools, and code navigation in your IDE, leading to a more productive and enjoyable coding experience.
  • Modern JavaScript Features: TypeScript supports the latest JavaScript features, allowing you to write cleaner and more efficient code.

In essence, TypeScript helps you write better JavaScript, faster and with fewer errors. It’s a skill that will serve you well in any modern web development environment.

Setting Up Your Development Environment

To get started, you’ll need a few things:

  • Node.js and npm (Node Package Manager): These are essential for running JavaScript and managing project dependencies. You can download them from nodejs.org.
  • A Code Editor: Visual Studio Code (VS Code) is highly recommended due to its excellent TypeScript support, but you can use any editor you prefer.
  • TypeScript Compiler: We’ll install this globally using npm.

Let’s install the TypeScript compiler globally:

npm install -g typescript

Verify the installation by checking the TypeScript version:

tsc -v

This should display the installed TypeScript version.

Creating Your Project

Now, let’s create a new project directory and initialize it with npm:

mkdir todo-app
cd todo-app
npm init -y

This creates a `todo-app` directory, navigates into it, and initializes an npm project with default settings. Next, we need to install TypeScript as a project dependency:

npm install typescript --save-dev

The `–save-dev` flag tells npm to save TypeScript as a development dependency. This means it’s only needed during development, not when the app is deployed.

Configuring TypeScript

To configure TypeScript for our project, we need to create a `tsconfig.json` file. This file tells the TypeScript compiler how to compile our TypeScript code. Run the following command to generate a default `tsconfig.json` file:

tsc --init

This creates a `tsconfig.json` file in your project root. You can customize this file to configure various compiler options. For our project, we’ll keep the default settings, but let’s take a look at some important options:

  • `compilerOptions.target`: Specifies the ECMAScript target version for the generated JavaScript code (e.g., “es5”, “es6”, “esnext”).
  • `compilerOptions.module`: Specifies the module system to use (e.g., “commonjs”, “esnext”).
  • `compilerOptions.outDir`: Specifies the output directory for the compiled JavaScript files.
  • `compilerOptions.strict`: Enables strict type-checking options. It’s generally a good idea to set this to `true`.

Creating the To-Do List App Structure

Let’s create the basic file structure for our app. We’ll have two main files:

  • `index.html`: The HTML file for our app’s user interface.
  • `src/index.ts`: The main TypeScript file where we’ll write our application logic.

Create these files in your project directory. Your project structure should now look like this:

todo-app/
├── index.html
├── package.json
├── package-lock.json
├── src/
│   └── index.ts
└── tsconfig.json

Writing the HTML (index.html)

Let’s start by creating the HTML structure for our to-do list. Open `index.html` and add the following code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>To-Do List</title>
    <link rel="stylesheet" href="style.css"> <!-- Link to your CSS file -->
</head>
<body>
    <div class="container">
        <h1>To-Do List</h1>
        <input type="text" id="todoInput" placeholder="Add a task...">
        <button id="addButton">Add</button>
        <ul id="todoList">
            <!-- Tasks will be added here -->
        </ul>
    </div>
    <script src="bundle.js"></script> <!-- Link to your bundled JavaScript file -->
</body>
</html>

This HTML creates a basic structure with a title, an input field for adding tasks, an “Add” button, and an unordered list (`ul`) to display the tasks. We’ve also included a link to a `style.css` file (which we’ll create later) for styling and a link to `bundle.js`, which will contain our compiled JavaScript code.

Writing the TypeScript (src/index.ts)

Now, let’s write the TypeScript code that will make our to-do list functional. Open `src/index.ts` and add the following code:


// Define a type for a todo item
interface Todo {
    id: number;
    text: string;
    completed: boolean;
}

// Get references to the HTML elements
const todoInput = document.getElementById('todoInput') as HTMLInputElement;
const addButton = document.getElementById('addButton') as HTMLButtonElement;
const todoList = document.getElementById('todoList') as HTMLUListElement;

// Initialize an array to store the todos
let todos: Todo[] = [];

// Function to render the todos
function renderTodos() {
    todoList.innerHTML = ''; // Clear the list

    todos.forEach(todo => {
        const listItem = document.createElement('li');
        listItem.innerHTML = `
            <input type="checkbox" ${todo.completed ? 'checked' : ''} data-id="${todo.id}">
            <span class="todo-text ${todo.completed ? 'completed' : ''}">${todo.text}</span>
            <button data-id="${todo.id}">Delete</button>
        `;
        todoList.appendChild(listItem);

        // Add event listeners for checkbox and delete button
        const checkbox = listItem.querySelector('input[type="checkbox"]') as HTMLInputElement;
        const deleteButton = listItem.querySelector('button') as HTMLButtonElement;

        checkbox.addEventListener('change', () => toggleComplete(todo.id));
        deleteButton.addEventListener('click', () => deleteTodo(todo.id));
    });
}

// Function to add a new todo
function addTodo() {
    const text = todoInput.value.trim();
    if (text !== '') {
        const newTodo: Todo = {
            id: Date.now(), // Use timestamp as a unique ID
            text: text,
            completed: false,
        };
        todos.push(newTodo);
        renderTodos();
        todoInput.value = ''; // Clear the input field
    }
}

// Function to toggle the complete status of a todo
function toggleComplete(id: number) {
    todos = todos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
    );
    renderTodos();
}

// Function to delete a todo
function deleteTodo(id: number) {
    todos = todos.filter(todo => todo.id !== id);
    renderTodos();
}

// Add event listener to the add button
addButton.addEventListener('click', addTodo);

// Initial render
renderTodos();

Let’s break down this code:

  • Interface `Todo`: This defines the structure of a todo item, including an `id`, `text`, and `completed` status.
  • Element References: We get references to the HTML elements we need to interact with: the input field, the add button, and the todo list. The `as HTMLInputElement`, `as HTMLButtonElement`, and `as HTMLUListElement` are type assertions, which tell TypeScript the specific type of the HTML element.
  • `todos` Array: This array stores our todo items.
  • `renderTodos()` Function: This function clears the todo list and then iterates through the `todos` array, creating a list item (`li`) for each todo. It also adds event listeners to the checkbox and delete button for each todo.
  • `addTodo()` Function: This function gets the text from the input field, creates a new todo object, adds it to the `todos` array, and calls `renderTodos()` to update the display.
  • `toggleComplete()` Function: This function toggles the `completed` status of a todo item.
  • `deleteTodo()` Function: This function removes a todo item from the `todos` array.
  • Event Listeners: We add an event listener to the add button to call the `addTodo()` function when clicked.
  • Initial Render: We call `renderTodos()` initially to display any existing todos (although we don’t have any yet).

Compiling and Running the App

Now, let’s compile our TypeScript code into JavaScript. Open your terminal and run the following command from your project directory:

tsc

This will compile the `src/index.ts` file and create a `index.js` file in the same directory (or the output directory you specified in `tsconfig.json`). To make the JavaScript code usable in your HTML, you’ll need to bundle it. We’ll use a simple bundler called `esbuild` for this tutorial. Install it using npm:

npm install esbuild --save-dev

Then, add a script to your `package.json` to bundle the code. Open `package.json` and add the following line to the “scripts” section:


"scripts": {
    "build": "esbuild src/index.ts --bundle --outfile=bundle.js"
},

Now, run the build script:

npm run build

This will bundle the `index.ts` file into a single `bundle.js` file in the project root. Finally, open `index.html` in your browser. You should see your to-do list app! You can add tasks, mark them as complete, and delete them.

Adding Styling (style.css)

To make our to-do list app look better, let’s add some CSS. Create a file named `style.css` in your project root and add the following CSS rules:


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

.container {
    background-color: #fff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    width: 400px;
}

h1 {
    text-align: center;
    color: #333;
}

input[type="text"] {
    width: 100%;
    padding: 10px;
    margin-bottom: 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    box-sizing: border-box; /* Important for width to include padding */
}

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

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

#todoList {
    list-style: none;
    padding: 0;
}

#todoList li {
    display: flex;
    align-items: center;
    padding: 10px 0;
    border-bottom: 1px solid #eee;
}

#todoList li:last-child {
    border-bottom: none;
}

#todoList input[type="checkbox"] {
    margin-right: 10px;
}

.todo-text {
    flex-grow: 1;
}

.completed {
    text-decoration: line-through;
    color: #888;
}

This CSS provides basic styling for the app, including a background color, a container for the to-do list, styling for the input field, the button, and the list items. The `completed` class styles completed tasks with a line-through and a muted color.

To see the styling, make sure you’ve linked the CSS file in your `index.html` file, as shown earlier.

Common Mistakes and How to Fix Them

Here are some common mistakes beginners make when building to-do list apps in TypeScript, and how to avoid them:

  • Incorrect Element Selection: Forgetting to use type assertions when selecting HTML elements. For example: `const todoInput = document.getElementById(‘todoInput’);` will result in `todoInput` being of type `HTMLElement | null`. This will cause errors when you try to use properties specific to an input element (like `value`). Fix: Use type assertions: `const todoInput = document.getElementById(‘todoInput’) as HTMLInputElement;`
  • Not Handling Empty Input: Not checking if the input field is empty before adding a task. Fix: Add a check in your `addTodo()` function: `if (text.trim() !== ”) { … }`
  • Incorrect Event Listener Usage: Attaching event listeners to the wrong elements or not understanding how they work. Fix: Double-check that you’re attaching event listeners to the correct elements (e.g., the add button and the checkboxes) and that your event handler functions are correctly defined and called.
  • Not Updating the Display: Forgetting to call `renderTodos()` after adding, deleting, or completing a task. Fix: Make sure to call `renderTodos()` after any state change that affects the display of the to-do list.
  • Incorrect ID Handling: Using the same ID for multiple elements or not using unique IDs. Fix: Use a unique ID for each todo item. A common approach is to use `Date.now()`.

Key Takeaways

  • TypeScript Fundamentals: You’ve learned how to define types, use interfaces, and work with basic TypeScript syntax.
  • DOM Manipulation: You’ve learned how to select HTML elements, add event listeners, and dynamically update the content of your web page.
  • Application Structure: You’ve seen how to structure a simple web application with HTML, CSS, and TypeScript.
  • State Management: You’ve learned how to manage the state of your to-do list using an array and how to update the display when the state changes.
  • Bundling: You’ve learned how to bundle your TypeScript code into a single JavaScript file for deployment.

FAQ

  1. Can I use this code in a React or Angular project? Yes, the core logic of the to-do list (the TypeScript code) can be adapted to React or Angular. You would need to adjust the way you handle the DOM (e.g., using JSX in React or template syntax in Angular) and the way you manage the state (e.g., using React’s state hooks or Angular’s data binding).
  2. How can I store the to-do list data persistently? Currently, the to-do list data is lost when you refresh the page. To store it persistently, you can use local storage, session storage, or a database. For local storage, you would serialize the `todos` array to JSON using `JSON.stringify()` before storing it and deserialize it using `JSON.parse()` when loading the app.
  3. How can I add more features? You can add features like:
    • Editing tasks
    • Prioritizing tasks
    • Adding due dates
    • Filtering tasks (e.g., show only incomplete tasks)
    • Sorting tasks
  4. Why am I getting a type error? Double-check your code for type mismatches. Make sure you’re using the correct types for variables, function parameters, and return values. The TypeScript compiler provides helpful error messages that can guide you to the source of the problem. Also, make sure that you’re using type assertions when you select elements from the DOM.
  5. How do I deploy this app? You can deploy your app to a hosting service like Netlify, Vercel, or GitHub Pages. You’ll need to build your project (using `npm run build`) and upload the contents of the output directory (the `bundle.js` file and the `index.html`, `style.css` files, and any other assets).

Building a to-do list app is a great way to practice your TypeScript skills and learn the fundamentals of web development. By following this tutorial, you’ve gained a practical understanding of how to use TypeScript to create interactive web applications. Remember to experiment, explore new features, and continue learning. The world of web development is constantly evolving, so keep building, keep coding, and keep learning, and you’ll be well on your way to creating amazing applications.