TypeScript Tutorial: Building a Simple Web Application for a Basic To-Do List

Tired of scattered sticky notes and forgotten tasks? In today’s fast-paced world, staying organized is crucial. A well-structured to-do list can be a game-changer, helping you prioritize, manage your time effectively, and boost your productivity. This tutorial will guide you through building a simple, yet functional, to-do list web application using TypeScript, a powerful superset of JavaScript. This project is perfect for beginners and intermediate developers looking to deepen their understanding of TypeScript and front-end development principles. By the end, you’ll have a practical application you can use daily and a solid foundation for more complex projects.

Why TypeScript?

Before we dive into the code, let’s discuss why we’re using TypeScript. JavaScript, while versatile, can become difficult to manage in larger projects. TypeScript adds static typing to JavaScript, which offers several key advantages:

  • Improved Code Quality: TypeScript catches potential errors during development, reducing runtime bugs.
  • Enhanced Readability: Type annotations make code easier to understand and maintain.
  • Better Developer Experience: Features like autocompletion and refactoring tools become more effective.
  • Scalability: TypeScript helps you build larger, more maintainable applications.

Setting Up Your Development Environment

To get started, you’ll need the following:

  • Node.js and npm: Install Node.js, which includes npm (Node Package Manager). You can download it from nodejs.org.
  • A Code Editor: Visual Studio Code (VS Code) is highly recommended, as it offers excellent TypeScript support.
  • Basic HTML, CSS, and JavaScript knowledge: While this tutorial focuses on TypeScript, a basic understanding of these technologies is helpful.

Let’s create a project directory and initialize it with npm:

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

This creates a `package.json` file, which manages your project dependencies.

Installing TypeScript

Next, install TypeScript globally or locally (we’ll use local installation for this tutorial):

npm install typescript --save-dev

This command installs TypeScript as a development dependency. Now, create a `tsconfig.json` file. This file configures the TypeScript compiler. You can generate a basic one using the following command:

npx tsc --init

This creates a `tsconfig.json` file in your project root. Open it and modify the following settings:

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

Here’s what these options mean:

  • target: Specifies the JavaScript version to compile to (ES5 is widely supported).
  • module: Specifies the module system (CommonJS is common for Node.js).
  • outDir: Specifies the output directory for compiled JavaScript files.
  • rootDir: Specifies the root directory of your TypeScript files.
  • strict: Enables strict type-checking.
  • esModuleInterop: Enables interoperability between CommonJS and ES modules.
  • skipLibCheck: Skips type checking of declaration files.
  • forceConsistentCasingInFileNames: Enforces consistent casing in filenames.
  • include: Specifies which files to include in the compilation.

Project Structure

Let’s create the following project structure:


todo-app/
├── src/
│   ├── index.ts        // Main application file
│   ├── styles.css      // Stylesheet
│   └── index.html      // HTML file
├── dist/
├── node_modules/
├── package.json
├── tsconfig.json

Creating the HTML Structure (index.html)

Create `src/index.html` with the following content:

<!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="styles.css">
</head>
<body>
    <div class="container">
        <h1>To-Do List</h1>
        <div class="input-container">
            <input type="text" id="taskInput" placeholder="Add a task...">
            <button id="addTaskButton">Add</button>
        </div>
        <ul id="taskList">
            <!-- Tasks will be added here -->
        </ul>
    </div>
    <script src="bundle.js"></script>
</body>
</html>

This HTML provides the basic structure for our to-do list. It includes:

  • A title and viewport meta tag.
  • A link to a stylesheet (`styles.css`).
  • A container div.
  • An input field and an “Add” button.
  • An unordered list (`ul`) to display tasks.
  • A script tag to include our JavaScript file (`bundle.js`). Note: We’ll generate this with a bundler later, but for now, we’ll reference it directly.

Styling the Application (styles.css)

Create `src/styles.css` and add some basic styles:


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-container {
    display: flex;
    margin-bottom: 10px;
}

#taskInput {
    flex-grow: 1;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    margin-right: 10px;
}

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

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

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

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

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

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

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

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

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

These styles provide a basic, clean look for our to-do list. Feel free to customize these styles further to match your preferences.

Writing the TypeScript Code (index.ts)

This is where the core logic of our application resides. Create `src/index.ts` and add the following code:


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

// Get references to HTML elements
const taskInput = document.getElementById('taskInput') as HTMLInputElement;
const addTaskButton = document.getElementById('addTaskButton') as HTMLButtonElement;
const taskList = document.getElementById('taskList') as HTMLUListElement;

// Initialize an array to hold tasks
let tasks: Task[] = [];

// Function to render tasks
function renderTasks(): void {
  taskList.innerHTML = ''; // Clear existing tasks
  tasks.forEach(task => {
    const listItem = document.createElement('li');
    listItem.innerHTML = `
      <span class="task-text ${task.completed ? 'completed' : ''}">${task.text}</span>
      <button class="delete-button" data-id="${task.id}">Delete</button>
    `;
    taskList.appendChild(listItem);

    // Add event listener for deleting tasks
    const deleteButton = listItem.querySelector('.delete-button') as HTMLButtonElement;
    deleteButton.addEventListener('click', () => deleteTask(task.id));

    // Add event listener for completing tasks (toggle completed class)
    const taskTextSpan = listItem.querySelector('.task-text') as HTMLSpanElement;
    taskTextSpan.addEventListener('click', () => toggleTaskCompletion(task.id));
  });
}

// Function to add a task
function addTask(): void {
  const taskText = taskInput.value.trim();
  if (taskText !== '') {
    const newTask: Task = {
      id: Date.now(), // Use timestamp as a unique ID
      text: taskText,
      completed: false,
    };
    tasks.push(newTask);
    taskInput.value = ''; // Clear the input field
    renderTasks();
  }
}

// Function to delete a task
function deleteTask(id: number): void {
  tasks = tasks.filter(task => task.id !== id);
  renderTasks();
}

// Function to toggle task completion
function toggleTaskCompletion(id: number): void {
    tasks = tasks.map(task =>
        task.id === id ? { ...task, completed: !task.completed } : task
    );
    renderTasks();
}

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

Let’s break down this code:

  1. Task Interface: Defines the structure of a task object (id, text, completed).
  2. Element References: Gets references to the HTML elements we’ll interact with. The as HTMLInputElement and as HTMLButtonElement assertions tell TypeScript the type of the elements.
  3. Tasks Array: An array to store our tasks.
  4. renderTasks() Function:
    • Clears the existing task list.
    • Iterates through the tasks array.
    • Creates a list item (<li>) for each task.
    • Sets the inner HTML of the list item, including the task text and a delete button. Note the use of template literals for easy string interpolation.
    • Adds the list item to the task list.
    • Adds event listeners to the delete button and the task text itself (for toggling completion).
  5. addTask() Function:
    • Gets the task text from the input field.
    • If the text is not empty:
    • Creates a new task object with a unique ID (using Date.now()), the text, and a completed status of false.
    • Adds the new task to the tasks array.
    • Clears the input field.
    • Calls renderTasks() to update the display.
  6. deleteTask(id: number) Function:
    • Filters the tasks array, keeping only tasks whose IDs do not match the provided id.
    • Calls renderTasks() to update the display.
  7. toggleTaskCompletion(id: number) Function:
    • Maps the tasks array, toggling the completed status of the task with the matching ID.
    • Calls renderTasks() to update the display.
  8. Event Listener: Adds an event listener to the “Add” button, calling the addTask() function when clicked.

Compiling and Running the Application

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

npx tsc

This command uses the TypeScript compiler (tsc) to transpile your TypeScript code into JavaScript, and the output will be placed into the directory specified by the outDir option in your tsconfig.json file (which is the dist directory in our example). Now, the generated JavaScript code is ready to be used in our HTML file. We can now modify the script tag in `index.html` to point to the correct file:

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

To run the application, you can open `src/index.html` in your web browser. However, it’s generally better to serve the files using a local server for proper functionality (especially if you’re using modules). Let’s use a simple server like `serve`:

npm install -g serve
serve -d .

This will start a local server, typically on port 5000 (or another available port). Open your browser and navigate to the address provided by `serve` (e.g., `http://localhost:5000`) to see your to-do list application in action. You should be able to add tasks, delete them, and mark them as complete.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to avoid them:

  • Incorrect Element Selection: Make sure your HTML element IDs match the IDs you’re using in your TypeScript code. Use the browser’s developer tools (right-click, “Inspect”) to check if the elements are being selected correctly.
  • Type Errors: TypeScript will highlight type errors during development. Carefully read the error messages and ensure your variables and function parameters have the correct types.
  • Event Listener Issues: Ensure your event listeners are correctly attached to the elements. Double-check your selectors (e.g., querySelector()) and make sure the functions you’re calling within the listeners are defined correctly. Also, make sure the event listener is attached *after* the element is added to the DOM.
  • Incorrect Path to JavaScript File: Verify that the path to your compiled JavaScript file (e.g., `dist/index.js`) in your `index.html` is correct. Check the browser’s developer console for any 404 errors (file not found).
  • Forgetting to Compile: Remember to recompile your TypeScript code (npx tsc) whenever you make changes to your `.ts` files.

Enhancements and Next Steps

This is a basic to-do list, and there are many ways you can extend it:

  • Local Storage: Save tasks to local storage so they persist across browser sessions.
  • Edit Tasks: Add functionality to edit existing tasks.
  • Prioritization: Implement task prioritization (e.g., high, medium, low).
  • Due Dates: Add due dates to tasks.
  • Categorization: Allow users to categorize tasks (e.g., work, personal).
  • Advanced Styling: Use CSS frameworks like Bootstrap or Tailwind CSS for more advanced styling and responsiveness.
  • Bundling with Webpack or Parcel: For larger projects, consider using a module bundler like Webpack or Parcel. This simplifies dependency management and allows for more advanced features like code splitting and asset optimization.

Key Takeaways

  • TypeScript adds static typing to JavaScript, improving code quality and readability.
  • Setting up a TypeScript project involves initializing with npm, installing TypeScript, and configuring a `tsconfig.json` file.
  • The core of the application logic resides in TypeScript files, where you define interfaces, handle events, and manipulate the DOM.
  • Compiling TypeScript code generates JavaScript files that can be run in a browser.
  • Error messages from the TypeScript compiler are your friends; they help you catch bugs early.

FAQ

  1. Why use TypeScript instead of JavaScript? TypeScript helps you write more maintainable, scalable, and less error-prone code by adding static typing and other features. This is especially beneficial for larger projects.
  2. How do I debug TypeScript code? You can use your browser’s developer tools to debug the compiled JavaScript code. TypeScript source maps help you relate the JavaScript code back to your original TypeScript code.
  3. 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.
  4. What are some good resources for learning TypeScript? The official TypeScript documentation (https://www.typescriptlang.org/docs/) is an excellent resource. You can also find numerous tutorials and courses on platforms like Udemy, Coursera, and freeCodeCamp.

Building this to-do list application provides a practical introduction to TypeScript and front-end development. By understanding the concepts and following the steps outlined in this tutorial, you’ve gained valuable experience in setting up a TypeScript project, working with HTML, CSS, and JavaScript, and building a functional web application. Now, armed with this knowledge, you can explore more advanced features, experiment with different frameworks, and continue to expand your skills in the world of web development. The power to create organized, efficient, and user-friendly applications is now within your reach. Keep coding, keep learning, and keep building.