TypeScript Tutorial: Building a Simple To-Do List Application

In the world of web development, managing tasks and staying organized is crucial. To-do lists are a fundamental tool for this, and building one can be a fantastic learning experience. This tutorial will guide you through creating a simple, yet functional, to-do list application using TypeScript. We’ll cover the core concepts, from setting up the project to implementing features like adding, deleting, and marking tasks as complete. By the end, you’ll have a solid understanding of TypeScript fundamentals and a practical application to show for it.

Why TypeScript for a To-Do List?

TypeScript, a superset of JavaScript, brings static typing to your code. This means you define the data types of your variables, function parameters, and return values. This provides several benefits:

  • Early Error Detection: TypeScript catches errors during development, preventing runtime surprises.
  • Improved Code Readability: Types make your code easier to understand and maintain.
  • Enhanced Refactoring: TypeScript makes it easier to refactor code with confidence.
  • Better Developer Experience: IDEs provide better autocompletion and suggestions, boosting productivity.

For a to-do list application, TypeScript helps ensure the data structure of your tasks is consistent, making it easier to manage and scale the application. It also prevents common JavaScript errors related to data types.

Setting Up Your TypeScript Project

Let’s start by setting 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.
  2. mkdir todo-app
     cd todo-app
  3. Initialize npm: Initialize a new npm project. This creates a package.json file to manage your project’s dependencies.
    npm init -y
  4. Install TypeScript: Install TypeScript as a development dependency.
    npm install typescript --save-dev
  5. Initialize TypeScript Configuration: Create a tsconfig.json file to configure TypeScript.
    npx tsc --init

    This command creates a tsconfig.json file with default settings. You can customize these settings to fit your project’s needs. For our to-do list, we’ll keep the defaults for now.

  6. Create Source Files: Create a directory named src and a file named index.ts inside it. This is where we’ll write our TypeScript code.
    mkdir src
     touch src/index.ts

Defining the Task Interface

Before we start coding the core logic, let’s define the structure of our tasks. We’ll use an interface to ensure consistency and type safety. Open src/index.ts and add the following code:

interface Task {
  id: number;
  text: string;
  completed: boolean;
}

This interface defines the properties of a task:

  • id: A unique number for each task.
  • text: The description of the task.
  • completed: A boolean indicating whether the task is completed or not.

Implementing the To-Do List Logic

Now, let’s implement the core functionality of our to-do list. We’ll need functions to add tasks, delete tasks, mark tasks as complete, and display the tasks. Add the following code to src/index.ts, below the Task interface:

let tasks: Task[] = [];

function addTask(text: string): void {
  const newTask: Task = {
    id: Date.now(), // Simple way to generate unique IDs
    text,
    completed: false,
  };
  tasks.push(newTask);
  renderTasks();
}

function deleteTask(id: number): void {
  tasks = tasks.filter((task) => task.id !== id);
  renderTasks();
}

function toggleComplete(id: number): void {
  tasks = tasks.map((task) =>
    task.id === id ? { ...task, completed: !task.completed } : task
  );
  renderTasks();
}

Let’s break down these functions:

  • tasks: An array to store our tasks, initialized as an empty array of type Task[].
  • addTask(text: string): void: Adds a new task to the tasks array. It takes the task text as a string, creates a new Task object, and pushes it to the array. It uses Date.now() to create a simple unique ID. Finally, it calls renderTasks() to update the display.
  • deleteTask(id: number): void: Removes a task from the tasks array based on its ID. It uses the filter method to create a new array containing only the tasks whose IDs do not match the provided ID. Then, it calls renderTasks().
  • toggleComplete(id: number): void: Toggles the completion status of a task. It uses the map method to iterate over the tasks array. If a task’s ID matches the provided ID, it creates a new task object with the completed property flipped. Otherwise, it returns the original task. Finally, it calls renderTasks().

Rendering the To-Do List in the Browser

Now, let’s create a function to render the tasks in the browser. This function will dynamically create HTML elements to display the tasks. Add the following code to src/index.ts, below the functions defined above:


const taskList: HTMLUListElement | null = document.getElementById('taskList') as HTMLUListElement | null;
const taskInput: HTMLInputElement | null = document.getElementById('taskInput') as HTMLInputElement | null;
const addButton: HTMLButtonElement | null = document.getElementById('addButton') as HTMLButtonElement | null;

function renderTasks(): void {
  if (!taskList) return;
  taskList.innerHTML = ''; // Clear the existing list

  tasks.forEach((task) => {
    const listItem = document.createElement('li');
    listItem.innerHTML = `
      
      <span>${task.text}</span>
      <button data-id="${task.id}">Delete</button>
    `;

    // Add event listeners
    const checkbox = listItem.querySelector('input[type="checkbox"]');
    const deleteButton = listItem.querySelector('button');

    if (checkbox) {
      checkbox.addEventListener('change', () => {
        const taskId = parseInt(checkbox.dataset.id || '0');
        toggleComplete(taskId);
      });
    }

    if (deleteButton) {
      deleteButton.addEventListener('click', () => {
        const taskId = parseInt(deleteButton.dataset.id || '0');
        deleteTask(taskId);
      });
    }

    taskList.appendChild(listItem);
  });
}

// Event listener for adding tasks
if (addButton && taskInput) {
  addButton.addEventListener('click', () => {
    if (taskInput.value.trim()) {
      addTask(taskInput.value.trim());
      taskInput.value = ''; // Clear input after adding
    }
  });
}

Here’s a breakdown of the rendering function:

  • Get DOM Elements: The code starts by retrieving references to the HTML elements we’ll be manipulating. This includes the task list (taskList), the input field (taskInput), and the add button (addButton). The as HTMLUListElement | null and similar type assertions are used to help TypeScript understand that we’re working with specific HTML element types and to handle the possibility of those elements not being found in the DOM (hence the | null).
  • Clear the List: Inside renderTasks(), the existing content of the taskList is cleared using taskList.innerHTML = ''. This ensures that the list is refreshed with the current tasks.
  • Iterate and Create List Items: The code iterates over the tasks array using forEach(). For each task, it creates a new list item (<li>) element.
  • Generate HTML: The inner HTML of the list item is set to include a checkbox (for marking tasks as complete), the task text, and a delete button. The data-id attribute is used to store the task ID for event handling.
  • Add Event Listeners: Event listeners are attached to the checkbox and delete button. When the checkbox is clicked, the toggleComplete() function is called. When the delete button is clicked, the deleteTask() function is called. The parseInt(checkbox.dataset.id || '0') safely extracts the task ID from the data-id attribute.
  • Append to List: The newly created list item is appended to the taskList.
  • Add Task Event Listener: An event listener is added to the add button. When the button is clicked, it checks if the input field has any text. If it does, the addTask() function is called, and the input field is cleared.

Creating the HTML Structure

Now, let’s create the HTML structure for our to-do list. Create an index.html file in the root of your project 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>
</head>
<body>
  <h1>To-Do List</h1>
  <input type="text" id="taskInput" placeholder="Add a task...">
  <button id="addButton">Add</button>
  <ul id="taskList"></ul>

  <script src="./dist/index.js"></script>
</body>
</html>

This HTML structure includes:

  • A heading for the title.
  • An input field (taskInput) for entering new tasks.
  • An add button (addButton) to add tasks to the list.
  • An unordered list (taskList) to display the tasks.
  • A link to the compiled JavaScript file (./dist/index.js).

Compiling and Running the Application

Now, it’s time to compile your TypeScript code into JavaScript and run the application. Follow these steps:

  1. Compile TypeScript: In your terminal, run the following command to compile your TypeScript code into JavaScript:
    npx tsc

    This command uses the TypeScript compiler (tsc) to transpile the src/index.ts file into a dist/index.js file. The tsconfig.json file controls the compilation process.

  2. Serve the Application: You can use a simple web server to serve your HTML and JavaScript files. One easy way to do this is to use the serve package. First, install it globally:
    npm install -g serve

    Then, navigate to your project directory in your terminal and run the following command to start the server:

    serve .

    This command will start a local web server, typically on port 5000 (or another available port). It will serve the files in your project directory.

  3. Open in Your Browser: Open your web browser and go to the address provided by the serve command (e.g., http://localhost:5000). You should see your to-do list application.
  4. Test the Application: Try adding tasks, marking them as complete, and deleting them. The application should function as expected.

Addressing Common Mistakes and Enhancements

Let’s look at some common mistakes and how to avoid or fix them, along with potential enhancements:

  • Typo Errors: TypeScript helps prevent typo errors, but it’s still possible to make them. Double-check your code for typos in variable names, function names, and property names. TypeScript will highlight these errors during compilation.
  • Incorrect Data Types: Make sure you’re using the correct data types. For example, if you’re expecting a number, don’t pass a string. TypeScript will catch these type errors.
  • Missing or Incorrect Imports: If you’re using external libraries, make sure you import them correctly. TypeScript can help you with auto-completion and type checking for imported modules.
  • DOM Manipulation Errors: When working with the DOM, make sure you’re selecting the correct elements and that your code is correctly manipulating them. Use the browser’s developer tools to inspect the HTML and JavaScript to identify any issues.
  • Enhancements:
    • Local Storage: Persist tasks in local storage so that they are saved even after the browser is closed.
    • Styling: Add CSS to style the to-do list and make it visually appealing.
    • Error Handling: Implement error handling to gracefully handle unexpected situations.
    • More Advanced Features: Add features like editing tasks, sorting tasks, and filtering tasks.

Key Takeaways

  • TypeScript Fundamentals: You’ve learned about interfaces, types, functions, and DOM manipulation.
  • Project Setup: You’ve learned how to set up a TypeScript project.
  • Code Organization: You’ve seen how to organize your code into functions and use modules to make it more manageable.
  • Debugging: You’ve learned how to use browser developer tools to debug your code.

FAQ

  1. Why use TypeScript instead of JavaScript?

    TypeScript adds static typing to JavaScript, which helps catch errors early, improves code readability, and makes it easier to refactor code. It also provides a better developer experience with IDE support.

  2. How do I install TypeScript?

    You can install TypeScript using npm: npm install typescript --save-dev

  3. How do I compile TypeScript code?

    You compile TypeScript code using the TypeScript compiler (tsc) command. For example, npx tsc compiles the code in your project based on the tsconfig.json file.

  4. How do I debug my TypeScript code?

    You can debug your TypeScript code using the browser’s developer tools. You may need to generate source maps (in your tsconfig.json) to map the compiled JavaScript code back to your original TypeScript code. Debugging tools in your IDE can also be used.

  5. Can I use TypeScript with existing JavaScript projects?

    Yes, you can gradually introduce TypeScript into your existing JavaScript projects. You can start by renaming your .js files to .ts and adding type annotations. TypeScript is designed to be compatible with JavaScript.

Building a to-do list application with TypeScript is a great way to solidify your understanding of the language. This tutorial has provided a solid foundation, from setting up your project to implementing core features. By adding features and exploring different styling options, you can further refine your skills and create a truly personalized and useful application. The ability to manage tasks effectively is a skill that translates into all areas of life, and the ability to build the tools to do so is a valuable skill in itself. So, keep experimenting, keep learning, and keep building!