TypeScript Tutorial: Creating a Simple To-Do List Application

In the world of web development, managing tasks and staying organized is crucial. To-do list applications are a fundamental tool for this, helping users track their goals, prioritize work, and improve productivity. This tutorial will guide you through creating a simple, yet functional, to-do list application using TypeScript. We’ll cover everything from setting up your development environment to implementing core features like adding, deleting, and marking tasks as complete. This hands-on project is perfect for beginners and intermediate developers looking to solidify their TypeScript skills and understand practical application development.

Why TypeScript for a To-Do List?

TypeScript, a superset of JavaScript, brings static typing to the language. This offers several benefits:

  • Enhanced Code Quality: TypeScript helps catch errors early in the development process, reducing runtime bugs.
  • Improved Readability: Types make your code easier to understand and maintain.
  • Better Developer Experience: Features like autocompletion and refactoring are significantly enhanced in TypeScript-enabled IDEs.

Building a to-do list app with TypeScript allows you to experience these advantages firsthand, making your code more robust and your development process more efficient.

Setting Up Your Development Environment

Before we dive into the code, let’s set up our environment. You’ll need:

  • Node.js and npm (or yarn): These are essential for managing packages and running our application. Download them from the official Node.js website.
  • A Code Editor: Visual Studio Code (VS Code) is highly recommended due to its excellent TypeScript support.

Once you have these installed, create a new project directory and initialize a Node.js project:

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

Next, install TypeScript and the TypeScript compiler:

npm install --save-dev typescript @types/node

The @types/node package provides type definitions for Node.js, allowing TypeScript to understand Node.js modules and APIs.

Create a tsconfig.json file in your project root to configure the TypeScript compiler. A basic configuration looks like this:

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

This configuration tells the compiler to:

  • Target ECMAScript 5 ("target": "es5").
  • Use the CommonJS module system ("module": "commonjs").
  • Output compiled JavaScript files to the dist directory ("outDir": "./dist").
  • Look for TypeScript files in the src directory ("rootDir": "./src").
  • Enable strict type checking ("strict": true").

Project Structure

Let’s set up a simple project structure:

todo-app/
├── src/
│   ├── index.ts
│   └── todo.ts
├── dist/
├── node_modules/
├── package.json
├── tsconfig.json
└── README.md

index.ts will be our main entry point, and todo.ts will hold our to-do list logic.

Creating the To-Do Item Interface

First, we’ll define an interface for our to-do items. This interface will specify the structure of each task:


// src/todo.ts

export interface TodoItem {
  id: number;
  text: string;
  completed: boolean;
}

This interface defines three properties: id (a number), text (a string representing the task), and completed (a boolean indicating whether the task is done).

Implementing the To-Do List Class

Next, we’ll create a TodoList class to manage our to-do items:


// src/todo.ts

import { TodoItem } from './todo';

export class TodoList {
  private todos: TodoItem[] = [];
  private nextId: number = 1;

  addTodo(text: string): TodoItem {
    const newTodo: TodoItem = {
      id: this.nextId++,
      text,
      completed: false,
    };
    this.todos.push(newTodo);
    return newTodo;
  }

  removeTodo(id: number): void {
    this.todos = this.todos.filter((todo) => todo.id !== id);
  }

  toggleComplete(id: number): void {
    const todo = this.todos.find((todo) => todo.id === id);
    if (todo) {
      todo.completed = !todo.completed;
    }
  }

  getTodos(): TodoItem[] {
    return this.todos;
  }
}

Here’s a breakdown of the TodoList class:

  • todos: TodoItem[]: An array to store our to-do items.
  • nextId: number: A counter to generate unique IDs for each task.
  • addTodo(text: string): TodoItem: Adds a new to-do item to the list.
  • removeTodo(id: number): void: Removes a to-do item by its ID.
  • toggleComplete(id: number): void: Toggles the completion status of a to-do item.
  • getTodos(): TodoItem[]: Returns the current list of to-do items.

Building the User Interface (UI) in HTML and JavaScript

For simplicity, we’ll create a basic HTML UI and use JavaScript to interact with our TypeScript code. Create an index.html file in your project root:


<!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">
</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"></ul>
    </div>
    <script src="dist/index.js"></script>
</body>
</html>

This HTML sets up the basic structure of the UI: a title, an input field for adding tasks, an add button, and an unordered list to display the tasks. Let’s add some basic styling in a file called style.css:


body {
    font-family: sans-serif;
    background-color: #f4f4f4;
    margin: 0;
    padding: 0;
    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: 80%;
    max-width: 500px;
}

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

input[type="text"] {
    width: 100%;
    padding: 10px;
    margin-bottom: 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-size: 16px;
}

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

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

ul {
    list-style: none;
    padding: 0;
}

li {
    padding: 10px;
    border-bottom: 1px solid #eee;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

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

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

.delete-button {
    background-color: #f44336;
    color: white;
    border: none;
    padding: 5px 10px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
}

.delete-button:hover {
    background-color: #da190b;
}

Now, let’s write the JavaScript code (index.ts) to interact with our TypeScript class and update the UI:


// src/index.ts
import { TodoList, TodoItem } from './todo';

const todoList = new TodoList();
const todoInput = document.getElementById('todoInput') as HTMLInputElement;
const addButton = document.getElementById('addButton') as HTMLButtonElement;
const todoListElement = document.getElementById('todoList') as HTMLUListElement;

function renderTodos(): void {
  todoListElement.innerHTML = '';
  todoList.getTodos().forEach((todo) => {
    const listItem = document.createElement('li');
    listItem.innerHTML = `
      <span class="${todo.completed ? 'completed' : ''}">${todo.text}</span>
      <button class="delete-button" data-id="${todo.id}">Delete</button>
    `;

    const span = listItem.querySelector('span') as HTMLSpanElement;
    span.addEventListener('click', () => {
      todoList.toggleComplete(todo.id);
      renderTodos();
    });

    const deleteButton = listItem.querySelector('.delete-button') as HTMLButtonElement;
    deleteButton.addEventListener('click', (event) => {
      const button = event.target as HTMLButtonElement;
      const todoId = parseInt(button.dataset.id || '0', 10);
      todoList.removeTodo(todoId);
      renderTodos();
    });

    todoListElement.appendChild(listItem);
  });
}

addButton.addEventListener('click', () => {
  const text = todoInput.value.trim();
  if (text) {
    todoList.addTodo(text);
    todoInput.value = '';
    renderTodos();
  }
});

renderTodos();

This code does the following:

  • Creates an instance of the TodoList class.
  • Gets references to the input field, add button, and to-do list element in the HTML.
  • Defines a renderTodos() function to display the to-do items in the UI.
  • Adds event listeners to the add button and to-do item spans for adding, completing, and deleting tasks.
  • Initializes the UI by rendering the initial to-do list.

Compiling and Running the Application

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

tsc

This command will compile your TypeScript files (index.ts and todo.ts) into JavaScript files in the dist directory. Now, open index.html in your browser. You should see your basic to-do list application, ready to use!

Adding More Features

While the basic application is functional, we can enhance it with more features:

1. Local Storage

To persist the to-do items even after the browser is closed, we can use local storage. Add the following functions to your index.ts file:


// src/index.ts

function saveTodos(): void {
  localStorage.setItem('todos', JSON.stringify(todoList.getTodos()));
}

function loadTodos(): void {
  const storedTodos = localStorage.getItem('todos');
  if (storedTodos) {
    const parsedTodos: TodoItem[] = JSON.parse(storedTodos);
    parsedTodos.forEach(todo => {
        todoList.addTodo(todo.text);
        if(todo.completed){
            todoList.toggleComplete(todo.id);
        }
    });
    renderTodos();
  }
}

Modify the renderTodos and the add button event listener to call saveTodos() and call loadTodos() at the start of the script:


// src/index.ts

// ... (previous code)

function renderTodos(): void {
  todoListElement.innerHTML = '';
  todoList.getTodos().forEach((todo) => {
    const listItem = document.createElement('li');
    listItem.innerHTML = `
      <span class="${todo.completed ? 'completed' : ''}">${todo.text}</span>
      <button class="delete-button" data-id="${todo.id}">Delete</button>
    `;

    const span = listItem.querySelector('span') as HTMLSpanElement;
    span.addEventListener('click', () => {
      todoList.toggleComplete(todo.id);
      renderTodos();
      saveTodos(); // Save after toggling
    });

    const deleteButton = listItem.querySelector('.delete-button') as HTMLButtonElement;
    deleteButton.addEventListener('click', (event) => {
      const button = event.target as HTMLButtonElement;
      const todoId = parseInt(button.dataset.id || '0', 10);
      todoList.removeTodo(todoId);
      renderTodos();
      saveTodos(); // Save after deleting
    });

    todoListElement.appendChild(listItem);
  });
}

addButton.addEventListener('click', () => {
  const text = todoInput.value.trim();
  if (text) {
    todoList.addTodo(text);
    todoInput.value = '';
    renderTodos();
    saveTodos(); // Save after adding
  }
});

loadTodos(); // Load todos on page load
renderTodos();

2. Filtering Tasks

Implement filtering options (e.g., “All