Function Types and Return Types
Focus: Type annotations, interfaces, function types, optional properties, union types
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.
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.
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();
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.
Task interfaceAt 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
}
tasks is an array of Task objects. nextId is a number. Add type annotations to both:
let tasks: Task[] = [];
let nextId: number = 1;
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.
Add parameter types and return types to every function:
createTask takes a string and returns a TaskaddTask takes a string and returns nothingdeleteTask takes a number and returns nothingtoggleTask takes a number and returns nothinggetStatusLabel takes a boolean and returns a stringrenderTask takes a Task and returns an HTMLLIElementrender takes nothing and returns nothingOnce you've added all the types above, check for any remaining red underlines in VS Code. Common ones at this stage:
input.value — TypeScript needs to know input is an HTMLInputElement, not just a generic HTMLElement, to allow .value(event: Event) => void or (event: SubmitEvent) => voidRun npm run build — if it completes with no errors, you're done.
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.
as HTMLInputElement is a type assertion — use it when you know more than TypeScript does about what getElementById returns.any. If you're tempted to write : any, that's a signal to stop and think about what the actual type should be.void is the return type for functions that don't return a value — use it for addTask, deleteTask, toggleTask, and render.npm run build is your final check. The dev server is more lenient than the TypeScript compiler. Always build before submitting.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/*

Built with ❤️ by the HackYourFuture community · Thank you, contributors
Found a mistake or have a suggestion? Let us know in the feedback form.