Week 3 - TypeScript

Type System

Interfaces vs Types

Generics

Type inference

Type Unions and Intersections

Function Types and Return Types

Utility Types

Type-Safe API Responses

Type Guards

HTML Element Types

Tooling

Practice

Assignment

Front end Track

Week 3 Assignment: Convert a Task List to TypeScript

Focus: Type annotations, interfaces, function types, optional properties, union types

Description

You have a small working task list app written in JavaScript. Your job is to convert it to TypeScript — without changing how the app behaves. The app already works. You are not adding features or fixing bugs. You are adding types.

This is exactly how TypeScript adoption happens in real teams: an existing JavaScript codebase gets converted file by file, with TypeScript catching mistakes along the way.

Setup

npm create vite@latest task-list-ts -- --template vanilla-ts
cd task-list-ts
npm install
npm run dev

Delete the contents of src/main.ts and src/style.css. You'll replace them with the starter code below.

Starter Code

Copy the following files into your project exactly as written. Do not change the logic — only add TypeScript.

index.html — replace the existing one in the project root:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Task List</title>
    <link rel="stylesheet" href="/src/style.css" />
  </head>
  <body>
    <div id="app">
      <h1>My Tasks</h1>

      <form id="task-form">
        <input
          type="text"
          id="task-input"
          placeholder="What needs to be done?"
          required
        />
        <button type="submit">Add task</button>
      </form>

      <ul id="task-list"></ul>

      <p id="empty-message">No tasks yet. Add one above!</p>
    </div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

src/style.css:

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  font-family: system-ui, sans-serif;
  max-width: 480px;
  margin: 48px auto;
  padding: 0 16px;
  color: #1a1a1a;
}

h1 {
  font-size: 1.5rem;
  margin-bottom: 24px;
}

form {
  display: flex;
  gap: 8px;
  margin-bottom: 24px;
}

input[type='text'] {
  flex: 1;
  padding: 8px 12px;
  font-size: 1rem;
  border: 1px solid #d1d5db;
  border-radius: 6px;
}

input[type='text']:focus {
  outline: none;
  border-color: #6366f1;
  box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
}

button {
  padding: 8px 16px;
  font-size: 1rem;
  background: #6366f1;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
}

button:hover {
  background: #4f46e5;
}

ul {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

li {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  background: white;
}

li.completed span {
  text-decoration: line-through;
  color: #9ca3af;
}

li span {
  flex: 1;
  font-size: 1rem;
}

li button {
  background: #ef4444;
  padding: 4px 10px;
  font-size: 0.875rem;
}

li button:hover {
  background: #dc2626;
}

#empty-message {
  color: #9ca3af;
  font-size: 0.9rem;
}

src/main.ts — this is the JavaScript logic you need to convert:

// ─── State ────────────────────────────────────────────────────────────────────

let tasks = [];
let nextId = 1;

// ─── DOM references ───────────────────────────────────────────────────────────

const form = document.getElementById('task-form');
const input = document.getElementById('task-input');
const list = document.getElementById('task-list');
const emptyMessage = document.getElementById('empty-message');

// ─── Functions ────────────────────────────────────────────────────────────────

function createTask(title) {
  return {
    id: nextId++,
    title: title,
    completed: false,
  };
}

function addTask(title) {
  const task = createTask(title);
  tasks.push(task);
  render();
}

function deleteTask(id) {
  tasks = tasks.filter(function (task) {
    return task.id !== id;
  });
  render();
}

function toggleTask(id) {
  tasks = tasks.map(function (task) {
    if (task.id === id) {
      return { ...task, completed: !task.completed };
    }
    return task;
  });
  render();
}

function getStatusLabel(completed) {
  if (completed) {
    return 'Done';
  }
  return 'Pending';
}

function renderTask(task) {
  const li = document.createElement('li');
  if (task.completed) {
    li.classList.add('completed');
  }

  const checkbox = document.createElement('input');
  checkbox.type = 'checkbox';
  checkbox.checked = task.completed;
  checkbox.addEventListener('change', function () {
    toggleTask(task.id);
  });

  const span = document.createElement('span');
  span.textContent = task.title;

  const status = document.createElement('small');
  status.textContent = getStatusLabel(task.completed);

  const deleteBtn = document.createElement('button');
  deleteBtn.textContent = 'Delete';
  deleteBtn.addEventListener('click', function () {
    deleteTask(task.id);
  });

  li.appendChild(checkbox);
  li.appendChild(span);
  li.appendChild(status);
  li.appendChild(deleteBtn);

  return li;
}

function render() {
  list.innerHTML = '';

  if (tasks.length === 0) {
    emptyMessage.style.display = 'block';
    return;
  }

  emptyMessage.style.display = 'none';
  tasks.forEach(function (task) {
    const li = renderTask(task);
    list.appendChild(li);
  });
}

// ─── Event listeners ──────────────────────────────────────────────────────────

form.addEventListener('submit', function (event) {
  event.preventDefault();
  const title = input.value.trim();
  if (title) {
    addTask(title);
    input.value = '';
  }
});

// ─── Init ─────────────────────────────────────────────────────────────────────

render();

Your Task

Run the app first with npm run dev and confirm it works. Then convert src/main.ts to TypeScript. The app must still work identically after your changes.

TypeScript will underline errors in VS Code as you work. Read each one carefully — they tell you exactly what type is missing or wrong.

Step 1 — Define a Task interface

At the top of the file, define an interface that describes the shape of a task object. A task has an id, a title, and a completed status.

interface Task {
  // fill this in
}

Step 2 — Type the state variables

tasks is an array of Task objects. nextId is a number. Add type annotations to both:

let tasks: Task[] = [];
let nextId: number = 1;

Step 3 — Type the DOM references

document.getElementById returns HTMLElement | null. TypeScript will complain if you try to call methods on a value that might be null. You have two options — pick whichever makes sense for each element:

Option A — non-null assertion (tells TypeScript "trust me, this exists"):

const form = document.getElementById('task-form') as HTMLFormElement;

Option B — null check (handle the missing case explicitly):

const input = document.getElementById('task-input');
if (!input) throw new Error('task-input not found');

You'll also need to cast to the right element type so TypeScript knows which properties are available. HTMLFormElement, HTMLInputElement, HTMLUListElement, and HTMLParagraphElement are the ones you need here.

Step 4 — Type the functions

Add parameter types and return types to every function:

Step 5 — Fix any remaining TypeScript errors

Once you've added all the types above, check for any remaining red underlines in VS Code. Common ones at this stage:

Run npm run build — if it completes with no errors, you're done.

Extension

If you finish early, add a createdAt property to the Task interface of type Date, and set it to new Date() in createTask. Then display it in renderTask using task.createdAt.toLocaleDateString(). TypeScript will guide you to update every place that creates or uses a task.

Hints


Submission

Follow the Assignment submission guide to learn how to submit the assignment


The HackYourFuture curriculum is licensed under CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

CC BY-NC-SA 4.0 Icons

Built with ❤️ by the HackYourFuture community · Thank you, contributors

Found a mistake or have a suggestion? Let us know in the feedback form.