TypeScript Tutorial: Building a Simple Web Application for Task Management

In today’s fast-paced world, staying organized is key. Whether you’re a student, a professional, or simply someone with a lot on their plate, effective task management can make a world of difference. This tutorial will guide you through building a simple, yet functional, task management web application using TypeScript. We’ll cover everything from setting up your development environment to creating interactive features, all while learning fundamental TypeScript concepts. This project is perfect for beginners and intermediate developers looking to deepen their understanding of TypeScript and build practical, real-world applications.

Why TypeScript?

Before we dive in, let’s talk about why we’re using TypeScript. TypeScript is a superset of JavaScript that adds static typing. This means you can define the types of variables, function parameters, and return values. This offers several benefits:

  • Early Error Detection: TypeScript catches type-related errors during development, before you even run your code. This saves time and frustration.
  • Improved Code Readability: Types make your code easier to understand and maintain, especially in larger projects.
  • Enhanced Code Completion: IDEs can provide better code completion and suggestions, thanks to the type information.
  • Refactoring Safety: TypeScript makes refactoring your code safer, as it helps you identify and fix potential issues.

In essence, TypeScript helps you write more robust, maintainable, and scalable code. Now, let’s get started!

Setting Up Your Development Environment

To follow along with this tutorial, you’ll need the following:

  • Node.js and npm (or yarn): You’ll need Node.js and npm (Node Package Manager) or yarn installed on your system. These are essential for managing project dependencies.
  • A Code Editor: A code editor like Visual Studio Code (VS Code), Sublime Text, or Atom is recommended. VS Code has excellent TypeScript support.
  • Basic HTML, CSS, and JavaScript Knowledge: While this tutorial focuses on TypeScript, some familiarity with HTML, CSS, and JavaScript will be helpful.

Step 1: Create a Project Directory

Create a new directory for your project and navigate into it using your terminal:

mkdir task-manager-app
cd task-manager-app

Step 2: Initialize a Node.js Project

Initialize a new Node.js project using npm:

npm init -y

This will create a `package.json` file, which will store your project’s metadata and dependencies.

Step 3: Install TypeScript

Install TypeScript as a development dependency:

npm install --save-dev typescript

Step 4: Create a `tsconfig.json` File

Create a `tsconfig.json` file in your project directory. This file configures the TypeScript compiler. You can generate a basic `tsconfig.json` file using the TypeScript compiler:

npx tsc --init

This command creates a `tsconfig.json` file with many commented-out options. You can customize these options to suit your needs. For this tutorial, we’ll use a basic configuration. Open `tsconfig.json` and make sure the following options are set (or uncomment them and set them):

{
  "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}
  • `target`: Specifies the ECMAScript target version for the emitted JavaScript.
  • `module`: Specifies the module code generation.
  • `outDir`: Specifies the output directory for the compiled JavaScript files.
  • `rootDir`: Specifies the root directory of your source files.
  • `strict`: Enables strict type-checking options.
  • `esModuleInterop`: Enables interoperability between CommonJS and ES modules.
  • `skipLibCheck`: Skips type checking of declaration files.
  • `forceConsistentCasingInFileNames`: Enforces consistent casing in file names.
  • `include`: Specifies the files to be included in the compilation.

Step 5: Create Project Structure

Create the following directory structure in your project:

task-manager-app/
├── src/
│   ├── index.ts
│   └── styles.css
├── dist/
├── package.json
├── tsconfig.json
└── index.html

The `src` directory will contain your TypeScript source code. The `dist` directory will hold the compiled JavaScript files. `index.html` will be the main HTML file for your application.

Building the Task Management Application

Now, let’s start building the application. We’ll break it down into several steps.

1. Creating the HTML Structure

Create the basic HTML structure in `index.html`:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Task Manager</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="container">
        <h1>Task Manager</h1>
        <div class="task-input">
            <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="dist/index.js"></script>
</body>
</html>

This HTML sets up the basic layout, including:

  • A title and a container.
  • An input field and a button to add tasks.
  • An unordered list (`ul`) to display the tasks.
  • A link to `styles.css` for styling.
  • A link to `dist/index.js`, where the compiled TypeScript code will reside.

2. Styling with CSS

Create a simple style sheet in `src/styles.css`:

body {
    font-family: sans-serif;
    margin: 0;
    padding: 0;
    background-color: #f4f4f4;
}

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

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

.task-input {
    margin-bottom: 20px;
    display: flex;
}

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

#addTaskButton {
    padding: 10px 15px;
    background-color: #4CAF50;
    color: white;
    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;
    justify-content: space-between;
    align-items: center;
}

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

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

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

This CSS provides basic styling for the task manager interface.

3. Writing TypeScript Code

Now, let’s write the TypeScript code in `src/index.ts`. This is where the core logic of the task manager will reside.


// 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 store tasks
let tasks: Task[] = [];

// Function to render tasks
function renderTasks() {
    taskList.innerHTML = ''; // Clear the task list
    tasks.forEach(task => {
        const listItem = document.createElement('li');
        listItem.innerHTML = `
            <span>${task.text}</span>
            <button class="delete-button" data-id="${task.id}">Delete</button>
        `;
        // Add event listener for delete button
        const deleteButton = listItem.querySelector('.delete-button') as HTMLButtonElement;
        deleteButton.addEventListener('click', () => deleteTask(task.id));
        taskList.appendChild(listItem);
    });
}

// Function to add a task
function addTask() {
    if (taskInput.value.trim() === '') {
        alert('Please enter a task.');
        return;
    }
    const newTask: Task = {
        id: Date.now(), // Generate a unique ID
        text: taskInput.value.trim(),
        completed: false,
    };
    tasks.push(newTask);
    renderTasks();
    taskInput.value = ''; // Clear the input field
}

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

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

// Initial render
renderTasks();

Let’s break down this code:

  • Task Interface: Defines the structure of a task object.
  • Element References: Gets references to the HTML elements we’ll be working with.
  • Tasks Array: An array to store the tasks.
  • 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 to display the task text and a delete button.
    • Adds an event listener to the delete button.
    • Appends the list item to the task list (`ul`).
  • addTask Function:
    • Checks if the input field is empty.
    • Creates a new task object.
    • Adds the new task to the `tasks` array.
    • Calls `renderTasks()` to update the UI.
    • Clears the input field.
  • deleteTask Function:
    • Filters the `tasks` array to remove the task with the specified ID.
    • Calls `renderTasks()` to update the UI.
  • Event Listener: Adds an event listener to the add button, calling the `addTask` function when clicked.
  • Initial Render: Calls `renderTasks()` to initially display any tasks.

4. Compiling and Running the Application

Now, let’s compile the TypeScript code and run the application:

Step 1: Compile the TypeScript code

Open your terminal and run the following command from your project directory:

npx tsc

This will compile your TypeScript code and generate a `index.js` file in the `dist` directory.

Step 2: Open the HTML file in your browser

Open `index.html` in your web browser. You should see the basic task manager interface with an input field, an add button, and an empty task list.

Step 3: Test the application

Enter a task in the input field and click the “Add” button. The task should appear in the task list. You should also be able to delete the tasks by clicking the delete button.

Adding More Features

Now that we have a basic task manager, let’s add some more features to make it more useful.

1. Marking Tasks as Complete

Let’s add a checkbox to each task to mark it as complete. Modify the `renderTasks()` function in `src/index.ts` to include a checkbox:


function renderTasks() {
    taskList.innerHTML = '';
    tasks.forEach(task => {
        const listItem = document.createElement('li');
        listItem.innerHTML = `
            <input type="checkbox" data-id="${task.id}" ${task.completed ? 'checked' : ''}>
            <span>${task.text}</span>
            <button class="delete-button" data-id="${task.id}">Delete</button>
        `;

        const checkbox = listItem.querySelector('input[type="checkbox"]') as HTMLInputElement;
        checkbox.addEventListener('change', () => toggleTaskCompletion(task.id));

        const deleteButton = listItem.querySelector('.delete-button') as HTMLButtonElement;
        deleteButton.addEventListener('click', () => deleteTask(task.id));

        taskList.appendChild(listItem);
    });
}

We’ve added a checkbox input element and used a conditional expression to set the `checked` attribute based on the `task.completed` property. We also added an event listener to the checkbox to call `toggleTaskCompletion()` when the checkbox state changes.

Next, we need to add the `toggleTaskCompletion()` function:


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

This function uses the `map` method to iterate over the `tasks` array and update the `completed` property of the task with the matching ID. The spread operator (`…task`) is used to create a new object with the updated `completed` property, and the existing properties are copied over. Finally, call `renderTasks()` to reflect the changes in the UI.

Add the following CSS to `src/styles.css` to style the completed tasks:


#taskList li {
    align-items: center;
}

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

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

Modify the `renderTasks` function to apply the `completed` class to the span element when the task is complete:


function renderTasks() {
    taskList.innerHTML = '';
    tasks.forEach(task => {
        const listItem = document.createElement('li');
        listItem.innerHTML = `
            <input type="checkbox" data-id="${task.id}" ${task.completed ? 'checked' : ''}>
            <span class="${task.completed ? 'completed' : ''}">${task.text}</span>
            <button class="delete-button" data-id="${task.id}">Delete</button>
        `;

        const checkbox = listItem.querySelector('input[type="checkbox"]') as HTMLInputElement;
        checkbox.addEventListener('change', () => toggleTaskCompletion(task.id));

        const deleteButton = listItem.querySelector('.delete-button') as HTMLButtonElement;
        deleteButton.addEventListener('click', () => deleteTask(task.id));

        taskList.appendChild(listItem);
    });
}

Now, compile your TypeScript code and refresh your browser. You should now be able to mark tasks as complete by clicking the checkboxes.

2. Local Storage

To persist the tasks even after the page is refreshed, we can use local storage. Local storage allows you to store data in the user’s browser.

Step 1: Save Tasks to Local Storage

Add a function to save tasks to local storage. Modify the `addTask`, `deleteTask`, and `toggleTaskCompletion` functions to call the `saveTasksToLocalStorage` function after each change to the `tasks` array:


const LOCAL_STORAGE_KEY = 'tasks';

function saveTasksToLocalStorage() {
    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(tasks));
}

Step 2: Load Tasks from Local Storage

Add a function to load tasks from local storage. Add this function before the `renderTasks` function:


function loadTasksFromLocalStorage() {
    const storedTasks = localStorage.getItem(LOCAL_STORAGE_KEY);
    if (storedTasks) {
        tasks = JSON.parse(storedTasks);
    }
}

Step 3: Call the Load Function

Call the `loadTasksFromLocalStorage()` function at the beginning of your script, before rendering any tasks:


loadTasksFromLocalStorage();
renderTasks();

Step 4: Update the other functions

Modify the `addTask`, `deleteTask`, and `toggleTaskCompletion` functions to call `saveTasksToLocalStorage()` after each change to the `tasks` array:


function addTask() {
    if (taskInput.value.trim() === '') {
        alert('Please enter a task.');
        return;
    }
    const newTask: Task = {
        id: Date.now(), // Generate a unique ID
        text: taskInput.value.trim(),
        completed: false,
    };
    tasks.push(newTask);
    renderTasks();
    taskInput.value = ''; // Clear the input field
    saveTasksToLocalStorage(); // Save to local storage
}

function deleteTask(id: number) {
    tasks = tasks.filter(task => task.id !== id);
    renderTasks();
    saveTasksToLocalStorage(); // Save to local storage
}

function toggleTaskCompletion(id: number) {
    tasks = tasks.map(task =>
        task.id === id ? { ...task, completed: !task.completed } : task
    );
    renderTasks();
    saveTasksToLocalStorage(); // Save to local storage
}

Now, compile your TypeScript code and refresh your browser. Your tasks will persist even after refreshing the page.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to fix them when working with TypeScript and web applications:

  • Type Errors: TypeScript is strict about types. If you see type errors in your IDE or during compilation, carefully check your variable assignments, function parameters, and return values. Make sure the types match. For example, if you’re getting an error that a string can’t be assigned to a number, double-check your code to ensure you’re using the correct data types.
  • Incorrect Element Type: When getting elements from the DOM using `document.getElementById()`, you need to cast the result to the correct type. For example, if you’re getting an input element, you should cast it to `HTMLInputElement` using the `as` keyword: `const taskInput = document.getElementById(‘taskInput’) as HTMLInputElement;`. This helps the TypeScript compiler understand the properties and methods available on that element.
  • Event Listener Issues: Make sure you are correctly attaching event listeners to the right elements. Double-check your element references and ensure the event listeners are added after the elements are created in the DOM.
  • Incorrect Path to the Compiled JavaScript File: Ensure the path to your compiled JavaScript file in your HTML (`<script src=”dist/index.js”></script>`) is correct. If the path is incorrect, your JavaScript code will not be executed.
  • Incorrect Use of `this`: When using `this` inside event listeners or methods, the context of `this` might not be what you expect. Use arrow functions to preserve the context of `this`.
  • Missing Dependencies: Make sure you have installed all the necessary dependencies using npm or yarn. Check your `package.json` file to confirm that all required packages are listed.
  • Local Storage Issues: When using local storage, be mindful of the data types you’re storing. Local storage only stores strings. When storing objects or arrays, use `JSON.stringify()` to convert them to strings before storing them, and use `JSON.parse()` to convert them back to objects or arrays when retrieving them.

Key Takeaways

  • TypeScript Fundamentals: You’ve learned how to define interfaces, use types, and leverage the benefits of static typing.
  • DOM Manipulation: You’ve gained experience in selecting elements from the DOM and manipulating them to build interactive user interfaces.
  • Event Handling: You’ve learned how to attach event listeners to elements to respond to user interactions.
  • Local Storage: You’ve learned how to use local storage to persist data even after the page is refreshed.
  • Project Structure: You’ve learned how to structure a basic web application project.

FAQ

Here are some frequently asked questions about this tutorial:

  1. Can I use a different framework or library?

    Yes, you can adapt this tutorial to use a framework like React, Angular, or Vue.js. However, the core TypeScript concepts will remain the same. The main difference will be how you structure your components and manage the DOM.

  2. How can I deploy this application?

    You can deploy this application to various platforms, such as Netlify, Vercel, or GitHub Pages. You’ll need to build your TypeScript code (using `npx tsc`) and then deploy the contents of the `dist` directory and `index.html` to the platform of your choice.

  3. How can I add more features?

    You can extend this application by adding features such as:

    • Task categories and prioritization
    • Due dates and reminders
    • User authentication
    • Integration with a backend API to store tasks in a database
  4. What are some good resources for learning more about TypeScript?

    Here are some excellent resources:

Building this task management application is just the beginning. The concepts and techniques you’ve learned here can be applied to a wide range of web development projects. Remember that practice is key, so keep building and experimenting with TypeScript. As you build more projects, you’ll become more comfortable with the language and its benefits. Consider adding features like filtering tasks by category or due date, or integrating with a backend to store your tasks in a database. The possibilities are endless, and with each new feature, you’ll further solidify your understanding of TypeScript and web development principles. The journey of a thousand lines of code begins with a single task.