TypeScript Tutorial: Building a Simple Interactive Web-Based Task Scheduler

In the fast-paced world of web development, managing tasks efficiently is paramount. Whether you’re a student juggling assignments, a professional coordinating projects, or simply someone trying to stay organized, a well-designed task scheduler can be a lifesaver. This tutorial will guide you through building a simple, interactive web-based task scheduler using TypeScript, a powerful superset of JavaScript. We’ll explore the core concepts, step-by-step implementation, and common pitfalls, equipping you with the knowledge to create your own effective task management tool.

Why TypeScript?

TypeScript brings several advantages to the table, making it an excellent choice for this project:

  • Static Typing: TypeScript introduces static typing, allowing you to catch errors during development rather than at runtime. This leads to more robust and maintainable code.
  • Improved Code Readability: Types enhance code readability, making it easier to understand the intent and structure of your code.
  • Enhanced Developer Experience: TypeScript provides features like autocompletion and refactoring, improving developer productivity.
  • Modern JavaScript Features: TypeScript supports the latest JavaScript features, allowing you to write cleaner and more concise code.

Project Setup

Let’s get started by setting up our project environment. We’ll use npm (Node Package Manager) to manage our dependencies and TypeScript to compile our code.

  1. Create a Project Directory: Create a new directory for your project and navigate into it using your terminal:
mkdir task-scheduler
cd task-scheduler
  1. Initialize npm: Initialize a new npm project by running the following command. This will create a package.json file. Accept the default values by pressing Enter for each prompt, or customize as needed:
npm init -y
  1. Install TypeScript: Install TypeScript as a development dependency:
npm install --save-dev typescript
  1. Initialize TypeScript Configuration: Create a tsconfig.json file to configure TypeScript compilation. Run the following command:
npx tsc --init

This command creates a tsconfig.json file with a lot of commented-out options. You can customize this file to control how TypeScript compiles your code. For this project, we’ll use the following basic configuration. Open tsconfig.json and ensure the following settings are present (or set to these values):

{
  "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}
  • target: "ES2015": Specifies the JavaScript version to compile to.
  • module: "commonjs": Specifies the module system to use.
  • outDir: "./dist": Specifies the output directory for compiled JavaScript files.
  • rootDir: "./src": Specifies the root directory of your TypeScript source files.
  • strict: true: Enables strict type checking.
  • esModuleInterop: true: Enables interoperability between CommonJS and ES modules.
  • skipLibCheck: true: Skips type checking of declaration files.
  • forceConsistentCasingInFileNames: true: Enforces consistent casing in filenames.
  • include: ["src/**/*"]: Specifies which files to include in the compilation.
  1. Create Source Directory: Create a src directory to hold your TypeScript source files:
mkdir src

Now, your project structure should look like this:


task-scheduler/
├── node_modules/
├── package.json
├── tsconfig.json
├── src/
└── dist/

Core Concepts

Before diving into the code, let’s understand the key concepts involved in building a task scheduler:

  • Task: A task represents an individual item to be scheduled. It typically includes properties like a description, due date, priority, and status (e.g., to-do, in progress, completed).
  • Task List: A collection of tasks. This is where you store and manage your tasks.
  • User Interface (UI): The visual representation of the task scheduler, allowing users to interact with the application. This includes elements for adding, viewing, editing, and deleting tasks.
  • Data Persistence (Optional): Storing task data so it persists across sessions. This can be done using local storage, a database, or other storage mechanisms. For this tutorial, we will focus on the in-memory task management, but we will provide guidance on adding persistence.

Building the Task Model

Let’s start by creating a Task model using TypeScript. This model will define the structure of our tasks.

  1. Create a Task Model File: Create a file named src/Task.ts and add the following code:
// src/Task.ts

export interface Task {
  id: number;
  description: string;
  dueDate: Date;
  priority: "high" | "medium" | "low";
  status: "to-do" | "in-progress" | "completed";
}

Explanation:

  • interface Task: Defines an interface named Task.
  • id: number: A unique identifier for the task.
  • description: string: A brief description of the task.
  • dueDate: Date: The date the task is due.
  • priority: "high" | "medium" | "low": The priority of the task, using a union type to restrict the possible values.
  • status: "to-do" | "in-progress" | "completed": The status of the task, also using a union type.

Implementing the Task List

Next, we’ll create a class to manage our task list. This class will handle adding, retrieving, updating, and deleting tasks.

  1. Create a Task List Class File: Create a file named src/TaskList.ts and add the following code:
// src/TaskList.ts
import { Task } from "./Task";

export class TaskList {
  private tasks: Task[] = [];
  private nextId: number = 1;

  addTask(description: string, dueDate: Date, priority: "high" | "medium" | "low") {
    const newTask: Task = {
      id: this.nextId,
      description,
      dueDate,
      priority,
      status: "to-do",
    };
    this.tasks.push(newTask);
    this.nextId++;
    return newTask;
  }

  getTasks(): Task[] {
    return this.tasks;
  }

  getTaskById(id: number): Task | undefined {
    return this.tasks.find((task) => task.id === id);
  }

  updateTask(id: number, updates: Partial): Task | undefined {
    const taskIndex = this.tasks.findIndex((task) => task.id === id);
    if (taskIndex === -1) {
      return undefined; // Task not found
    }
    this.tasks[taskIndex] = {
      ...this.tasks[taskIndex],
      ...updates,
    };
    return this.tasks[taskIndex];
  }

  deleteTask(id: number): boolean {
    const initialLength = this.tasks.length;
    this.tasks = this.tasks.filter((task) => task.id !== id);
    return this.tasks.length < initialLength;
  }
}

Explanation:

  • import { Task } from "./Task": Imports the Task interface we defined earlier.
  • private tasks: Task[] = []: A private array to store our tasks.
  • private nextId: number = 1: A private variable to generate unique IDs for tasks.
  • addTask(description: string, dueDate: Date, priority: "high" | "medium" | "low"): Adds a new task to the list.
  • getTasks(): Task[]: Returns the list of tasks.
  • getTaskById(id: number): Task | undefined: Retrieves a task by its ID.
  • updateTask(id: number, updates: Partial): Task | undefined: Updates an existing task. The Partial type allows updating only specific properties of the task.
  • deleteTask(id: number): boolean: Deletes a task from the list.

Building the User Interface (UI)

Now, let’s create a simple UI using HTML, CSS, and JavaScript to interact with our task list. For simplicity, we’ll keep the UI basic, focusing on functionality.

  1. Create an HTML File: Create a file named index.html in the root directory 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>Task Scheduler</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="container">
    <h1>Task Scheduler</h1>

    <div class="add-task-form">
      <label for="description">Description:</label>
      <input type="text" id="description" name="description">

      <label for="dueDate">Due Date:</label>
      <input type="date" id="dueDate" name="dueDate">

      <label for="priority">Priority:</label>
      <select id="priority" name="priority">
        <option value="high">High</option>
        <option value="medium">Medium</option>
        <option value="low">Low</option>
      </select>

      <button id="addTaskButton">Add Task</button>
    </div>

    <div class="task-list">
      <ul id="taskList">
        <!-- Tasks will be displayed here -->
      </ul>
    </div>
  </div>
  <script src="dist/index.js"></script>
</body>
</html>

This HTML provides the basic structure for the UI, including input fields for adding tasks and a list to display the tasks. We’ve also included a link to a style.css file (which we’ll create next) and a script tag to include our compiled JavaScript file (dist/index.js).

  1. Create a CSS File: Create a file named style.css in the root directory and add some basic styling:
/* style.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;
}

.add-task-form {
  margin-bottom: 20px;
}

label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

input[type="text"], input[type="date"], select {
  width: 100%;
  padding: 8px;
  margin-bottom: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
}

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

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

.task-list {
}

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

.task-list li {
  padding: 10px;
  margin-bottom: 5px;
  background-color: #eee;
  border-radius: 4px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.task-list li button {
  background-color: #f44336;
  color: white;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
}
  1. Implement the UI Logic with TypeScript: Create a file named src/index.ts and add the following code. This file will handle the interaction between the UI and our TaskList class:
// src/index.ts
import { TaskList } from "./TaskList";
import { Task } from "./Task";

const taskList = new TaskList();

const descriptionInput = document.getElementById("description") as HTMLInputElement;
const dueDateInput = document.getElementById("dueDate") as HTMLInputElement;
const prioritySelect = document.getElementById("priority") as HTMLSelectElement;
const addTaskButton = document.getElementById("addTaskButton") as HTMLButtonElement;
const taskListElement = document.getElementById("taskList") as HTMLUListElement;

function renderTasks() {
  taskListElement.innerHTML = ""; // Clear existing tasks
  const tasks = taskList.getTasks();
  tasks.forEach((task) => {
    const listItem = document.createElement("li");
    listItem.innerHTML = `
      <span>${task.description} - Due: ${task.dueDate.toLocaleDateString()} - Priority: ${task.priority}</span>
      <button data-id="${task.id}">Delete</button>
    `;
    const deleteButton = listItem.querySelector("button") as HTMLButtonElement;
    deleteButton.addEventListener("click", () => {
      const taskId = parseInt(deleteButton.dataset.id || "0", 10);
      taskList.deleteTask(taskId);
      renderTasks();
    });
    taskListElement.appendChild(listItem);
  });
}

addTaskButton.addEventListener("click", () => {
  const description = descriptionInput.value;
  const dueDate = new Date(dueDateInput.value);
  const priority = prioritySelect.value as "high" | "medium" | "low";

  if (description && !isNaN(dueDate.getTime()) && priority) {
    taskList.addTask(description, dueDate, priority);
    renderTasks();
    // Clear the input fields after adding a task
    descriptionInput.value = "";
    dueDateInput.value = "";
    prioritySelect.value = "medium";
  }
});

renderTasks(); // Initial render

Explanation:

  • Imports the TaskList class and the Task interface.
  • Creates an instance of the TaskList class.
  • Gets references to the HTML elements.
  • renderTasks(): This function clears the task list and then iterates through the tasks, creating an HTML list item (<li>) for each task. It displays the task description, due date, and priority and adds a delete button.
  • Event listener for the delete button: When the delete button is clicked, it gets the task ID, calls the deleteTask() method of the TaskList, and re-renders the task list.
  • Event listener for the add task button: This listener gets the values from the input fields, validates them, calls the addTask() method of the TaskList, and re-renders the task list. It also clears the input fields after adding a task.
  • renderTasks(): Calls the render tasks function to populate the task list on page load.

Compiling and Running the Application

Now that we have written our TypeScript code, let’s compile it and run the application.

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

This command will use the configuration in your tsconfig.json file to compile the TypeScript files in the src directory into JavaScript files in the dist directory.

  1. Open the HTML file: Open the index.html file in your web browser. You should see the basic UI of the task scheduler.
  1. Test the Application: Add tasks by entering a description, due date, and selecting a priority, and then click the “Add Task” button. The tasks should appear in the task list. You can also delete tasks by clicking the “Delete” button next to each task.

Adding Data Persistence (Optional)

Currently, the task data is stored in memory and is lost when the browser is closed or refreshed. To make the task scheduler more useful, we can add data persistence using local storage. Local storage allows you to store data in the user’s browser, so it persists across sessions.

  1. Modify TaskList Class: Update the TaskList class to load tasks from local storage when the class is initialized and save tasks to local storage whenever the task list is modified.
// src/TaskList.ts
import { Task } from "./Task";

const STORAGE_KEY = "tasks";

export class TaskList {
  private tasks: Task[] = [];
  private nextId: number = 1;

  constructor() {
    this.loadTasks();
  }

  private loadTasks() {
    const storedTasks = localStorage.getItem(STORAGE_KEY);
    if (storedTasks) {
      try {
        this.tasks = JSON.parse(storedTasks) as Task[];
        // Find the highest ID and increment by one to set nextId.
        this.nextId = this.tasks.reduce((maxId, task) => Math.max(maxId, task.id), 0) + 1;
      } catch (error) {
        console.error("Error parsing tasks from local storage:", error);
        this.tasks = []; // Reset tasks if there's an error
      }
    }
  }

  private saveTasks() {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(this.tasks));
  }

  addTask(description: string, dueDate: Date, priority: "high" | "medium" | "low") {
    const newTask: Task = {
      id: this.nextId,
      description,
      dueDate,
      priority,
      status: "to-do",
    };
    this.tasks.push(newTask);
    this.nextId++;
    this.saveTasks(); // Save after adding
    return newTask;
  }

  getTasks(): Task[] {
    return this.tasks;
  }

  getTaskById(id: number): Task | undefined {
    return this.tasks.find((task) => task.id === id);
  }

  updateTask(id: number, updates: Partial): Task | undefined {
    const taskIndex = this.tasks.findIndex((task) => task.id === id);
    if (taskIndex === -1) {
      return undefined; // Task not found
    }
    this.tasks[taskIndex] = {
      ...this.tasks[taskIndex],
      ...updates,
    };
    this.saveTasks(); // Save after updating
    return this.tasks[taskIndex];
  }

  deleteTask(id: number): boolean {
    const initialLength = this.tasks.length;
    this.tasks = this.tasks.filter((task) => task.id !== id);
    this.saveTasks(); // Save after deleting
    return this.tasks.length < initialLength;
  }
}

Explanation of changes:

  • STORAGE_KEY: A constant to store the key for local storage.
  • constructor(): The constructor now loads tasks from local storage when the TaskList is created.
  • loadTasks(): Retrieves tasks from local storage using localStorage.getItem(), parses them using JSON.parse(), and updates the tasks array. It also handles potential errors during parsing. It also sets the nextId to be one greater than the highest existing ID, so new tasks get unique IDs.
  • saveTasks(): Converts the tasks array to a JSON string using JSON.stringify() and stores it in local storage using localStorage.setItem().
  • The addTask(), updateTask(), and deleteTask() methods now call saveTasks() after modifying the task list to persist the changes in local storage.
  1. Modify index.ts: Remove the instantiation of taskList and add a call to the loadTasks() method. Also, you don’t need to call renderTasks() when you initialize the TaskList because it is now done in the constructor.
// src/index.ts
import { TaskList } from "./TaskList";
import { Task } from "./Task";

const taskList = new TaskList();

const descriptionInput = document.getElementById("description") as HTMLInputElement;
const dueDateInput = document.getElementById("dueDate") as HTMLInputElement;
const prioritySelect = document.getElementById("priority") as HTMLSelectElement;
const addTaskButton = document.getElementById("addTaskButton") as HTMLButtonElement;
const taskListElement = document.getElementById("taskList") as HTMLUListElement;

function renderTasks() {
  taskListElement.innerHTML = ""; // Clear existing tasks
  const tasks = taskList.getTasks();
  tasks.forEach((task) => {
    const listItem = document.createElement("li");
    listItem.innerHTML = `
      <span>${task.description} - Due: ${task.dueDate.toLocaleDateString()} - Priority: ${task.priority}</span>
      <button data-id="${task.id}">Delete</button>
    `;
    const deleteButton = listItem.querySelector("button") as HTMLButtonElement;
    deleteButton.addEventListener("click", () => {
      const taskId = parseInt(deleteButton.dataset.id || "0", 10);
      taskList.deleteTask(taskId);
      renderTasks();
    });
    taskListElement.appendChild(listItem);
  });
}

addTaskButton.addEventListener("click", () => {
  const description = descriptionInput.value;
  const dueDate = new Date(dueDateInput.value);
  const priority = prioritySelect.value as "high" | "medium" | "low";

  if (description && !isNaN(dueDate.getTime()) && priority) {
    taskList.addTask(description, dueDate, priority);
    renderTasks();
    // Clear the input fields after adding a task
    descriptionInput.value = "";
    dueDateInput.value = "";
    prioritySelect.value = "medium";
  }
});

renderTasks(); // Initial render
  1. Recompile and Test: Recompile your TypeScript code with tsc and refresh your browser. Add some tasks, refresh the page, and verify that the tasks persist.

With these changes, the task data will be saved in your browser’s local storage and will be available even after you close and reopen the browser or refresh the page.

Common Mistakes and How to Fix Them

When working on this project, you might encounter some common mistakes. Here’s a look at some of them and how to resolve them:

  • Type Errors: TypeScript’s static typing can help catch errors during development. If you see type errors in your IDE or during compilation, carefully review the error messages. They usually indicate a mismatch between the expected and actual types. Ensure that the types of your variables, function parameters, and return values are consistent.
  • Incorrect Date Handling: Dates can be tricky in JavaScript. Make sure you’re handling dates correctly. When creating a new Date object from the input, ensure that the input format is compatible. Use toLocaleDateString() to display dates in a user-friendly format.
  • UI Element Access Errors: If you’re having trouble accessing UI elements (like input fields or buttons), double-check that you’re using the correct IDs in your document.getElementById() calls. Also, ensure that the HTML elements are loaded before your JavaScript code tries to access them. Consider placing your script tag at the end of the <body> tag or using the DOMContentLoaded event to ensure that the DOM is fully loaded before your script runs.
  • Incorrect Event Handling: When adding event listeners, make sure you’re attaching them to the correct elements and that the event handler functions are defined correctly. For example, if you’re using a delete button, ensure that you’re correctly retrieving the task ID from the button’s data attribute.
  • Local Storage Issues: If you’re implementing local storage, ensure that you’re stringifying your data correctly before saving it (using JSON.stringify()) and parsing it correctly when retrieving it (using JSON.parse()). Also, be aware of the storage limits of local storage.

Summary / Key Takeaways

In this tutorial, we’ve built a simple, interactive task scheduler using TypeScript. We covered the basics of setting up a TypeScript project, creating models, implementing a task list, building a UI, and adding data persistence using local storage. This project demonstrates the power and benefits of using TypeScript for web development.

Here are the key takeaways:

  • TypeScript for Robustness: TypeScript enhances code quality and maintainability through static typing.
  • Modular Design: Organizing your code into classes and interfaces makes it easier to manage and extend.
  • Event Handling: Understanding how to handle events is crucial for creating interactive web applications.
  • Data Persistence: Local storage allows you to save user data, providing a better user experience.
  • UI Development: Building a simple UI with HTML, CSS, and JavaScript is essential for user interaction.

FAQ

Here are answers to some frequently asked questions:

  1. Can I use a different UI framework?
    Yes, you can use any UI framework or library you prefer (e.g., React, Angular, Vue.js). The core logic of the task scheduler (the TaskList class and the Task model) can be adapted to work with any framework. You would need to adjust the UI implementation to use the framework’s components and event handling mechanisms.
  2. How can I add more features?
    You can extend this project by adding features such as:

    • Task categories/tags.
    • Recurring tasks.
    • Due date reminders.
    • Drag-and-drop task reordering.
    • Integration with a backend database.
  3. How can I deploy this application?
    You can deploy your task scheduler to a web server (e.g., Netlify, Vercel, or a traditional web server). You’ll need to build your TypeScript code (tsc), and then upload the HTML, CSS, and JavaScript files to the server. For more complex deployments, you might need to configure a build process and use a deployment tool.
  4. What are the best practices for larger applications?
    For larger applications, consider:

    • Using a state management library (e.g., Redux, Zustand) for managing application state.
    • Implementing a more sophisticated UI framework (e.g., React, Angular, Vue.js).
    • Using a build tool (e.g., Webpack, Parcel) for bundling and optimizing your code.
    • Implementing unit tests and integration tests.
    • Following a well-defined code style and using a linter (e.g., ESLint).

Building this task scheduler is a great way to learn and practice TypeScript and web development principles. As you expand your skills, you can continue to add more features and explore advanced concepts. The foundation you’ve built here will serve as a solid base for future projects.