CAPSTONE PROJECT · LESSON 24 OF 24
Build, publish and continue learning
Prepare the browser project for static hosting and review the complete learning path.
- release checks
- static hosting
- source maps
- next steps
Your runnable checkpoint
Extract into a new practice folder. Each download includes complete source, package.json, tsconfig.json and an offline lesson. Use Node.js 22 or 24. Each checkpoint is independent.
Install with npm install. Run npm run check, then npm run build. Terminal lessons use npm start; browser lessons use npm run serve and http://127.0.0.1:4173.
1. Understand the concepts
Use this final checkpoint as the same complete browser project with a release workflow. Run npm install, npm run check and npm run build. Review the earlier domain tests before changing task behavior. In a repository, commit package-lock.json and use npm ci for repeatable installs.
For static hosting, publish index.html together with the dist directory, preserving their relative paths. The HTML points to ./dist/main.js, which imports ./tasks.js. TypeScript source and node_modules are not required by the browser. The local server.mjs is a practice server, not your production hosting process.
Use HTTPS for the published application so browser security features such as crypto.randomUUID are available. Check add, toggle, delete, refresh and invalid-storage recovery on the actual URL. A different origin has separate storage, so local practice tasks will not automatically appear online.
Use npm run check as a CI gate and run behavior tests separately. Inspect browser errors and network failures when a deployment appears blank. A missing JavaScript file, wrong MIME type or wrong relative path can break an otherwise well-typed application.
You have covered the main language tools and a complete application workflow. Continue into framework-specific typing, declaration authoring, project references, performance and decorators as your projects require them. Decorators are a specialized feature with differing modern and legacy semantics; avoid enabling legacy flags without understanding a library’s requirements.
For your next independent project, build a course enrollment dashboard: typed student and course records, discriminated loading states, a validated API boundary, reusable table helpers and tests. Explain which guarantees come from static types and which require runtime checks.
2. Build and run
These are complete files. Preserve the paths shown below, including the src directory. The package and compiler configuration are included at the end.
src/main.ts
import { createTask, parseTasks, removeTask, toggleTask } from "./tasks.js";
import type { Task } from "./tasks.js";
const form = document.querySelector("#task-form");
const input = document.querySelector("#title");
const list = document.querySelector("#tasks");
const message = document.querySelector("#message");
if (!(form instanceof HTMLFormElement) || !(input instanceof HTMLInputElement)
|| !(list instanceof HTMLUListElement) || !(message instanceof HTMLParagraphElement)) {
throw new Error("Required page elements are missing");
}
const taskList = list;
const status = message;
const storageKey = "edupoly-typescript-tasks-v1";
let tasks: Task[] = [];
try {
const stored = localStorage.getItem(storageKey);
if (stored !== null) tasks = parseTasks(JSON.parse(stored));
} catch { status.textContent = "Saved tasks could not be loaded. New changes will replace invalid saved data."; }
function saveAndRender(next: Task[]): void {
tasks = next;
try {
localStorage.setItem(storageKey, JSON.stringify(tasks));
status.textContent = "Tasks saved";
} catch { status.textContent = "Storage unavailable. Changes last only until this page closes."; }
render();
}
function render(): void {
taskList.replaceChildren();
for (const task of tasks) {
const item = document.createElement("li");
const label = document.createElement("span");
label.textContent = `${task.done ? "✓" : "○"} ${task.title} `;
const toggle = document.createElement("button");
toggle.type = "button";
toggle.textContent = task.done ? "Undo" : "Complete";
toggle.setAttribute("aria-label", `${toggle.textContent} ${task.title}`);
toggle.addEventListener("click", () => saveAndRender(toggleTask(tasks, task.id)));
const remove = document.createElement("button");
remove.type = "button";
remove.textContent = "Delete";
remove.setAttribute("aria-label", `Delete ${task.title}`);
remove.addEventListener("click", () => saveAndRender(removeTask(tasks, task.id)));
item.append(label, toggle, remove);
taskList.append(item);
}
}
form.addEventListener("submit", event => {
event.preventDefault();
try {
const task = createTask(crypto.randomUUID(), input.value);
saveAndRender([...tasks, task]);
form.reset();
input.focus();
} catch (error: unknown) { status.textContent = error instanceof Error ? error.message : "Could not add task"; }
});
render();
src/tasks.ts
export type Task = { readonly id: string; title: string; done: boolean };
export function createTask(id: string, title: string): Task {
const clean = title.trim();
if (!id.trim()) throw new Error("Id is required");
if (clean.length < 1 || clean.length > 120) throw new Error("Use 1–120 characters");
return { id, title: clean, done: false };
}
export function toggleTask(tasks: readonly Task[], id: string): Task[] {
return tasks.map(task => task.id === id ? { ...task, done: !task.done } : task);
}
export function removeTask(tasks: readonly Task[], id: string): Task[] {
return tasks.filter(task => task.id !== id);
}
export function parseTasks(value: unknown): Task[] {
if (!Array.isArray(value)) throw new Error("Expected a task array");
const ids = new Set<string>();
return value.map((item: unknown) => {
if (typeof item !== "object" || item === null
|| !("id" in item) || typeof item.id !== "string"
|| !("title" in item) || typeof item.title !== "string"
|| !("done" in item) || typeof item.done !== "boolean") throw new Error("Invalid task");
const task = createTask(item.id, item.title);
if (ids.has(task.id)) throw new Error("Duplicate task id");
ids.add(task.id);
return { ...task, done: item.done };
});
}
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>EduPoly TypeScript Practice</title>
<style>body{font:18px system-ui;max-width:700px;margin:40px auto;padding:20px;background:#f6f8ff;color:#17263d}input,button{font:inherit;padding:10px}li{margin:12px 0}button{cursor:pointer}label{display:block;margin-bottom:8px}</style></head>
<body><h1>TypeScript task practice</h1><form id="task-form"><label for="title">Task title</label><input id="title" maxlength="120" required><button>Add task</button></form><p id="message" role="status"></p><ul id="tasks"></ul><script type="module" src="./dist/main.js"></script></body></html>
server.mjs
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { resolve, sep, extname } from "node:path";
const root = resolve(".");
const types = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".map": "application/json" };
createServer(async (request, response) => {
try {
const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://localhost").pathname);
const file = resolve(root, "." + (pathname === "/" ? "/index.html" : pathname));
if (!file.startsWith(root + sep)) { response.writeHead(403).end(); return; }
const content = await readFile(file);
response.writeHead(200, { "Content-Type": types[extname(file)] ?? "application/octet-stream" });
response.end(content);
} catch { response.writeHead(404).end("Not found"); }
}).listen(4173, "127.0.0.1", () => console.log("Open http://127.0.0.1:4173"));
package.json
{
"name": "edupoly-typescript-build-publish-and-next-steps",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"check": "tsc --noEmit",
"build": "tsc",
"serve": "node server.mjs"
},
"devDependencies": {
"typescript": "5.9.3"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": [
"ES2022",
"DOM"
],
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noEmitOnError": true,
"verbatimModuleSyntax": true,
"rootDir": "src",
"outDir": "dist",
"sourceMap": true,
"types": [],
"moduleDetection": "force"
},
"include": [
"src/**/*"
]
}
3. Trace the code and check the result
- Only generated JavaScript runs in the deployed browser.
- The storage key is versioned so future schema changes can be migrated deliberately.
- Type checking, behavior tests and live smoke checks provide different evidence.
Expected result
Release checklist: type check passes; emitted modules load over HTTPS; task actions and persistence work on the hosted origin.4. Practice independently
Design a version 2 storage migration that adds a priority field.
Show the explanation
Read the old version, validate its existing fields, create new objects with priority: "normal", validate the new shape, then save under a new versioned key. Keep a recovery path if parsing or saving fails.
When something goes wrong
If tsc is missing, run npm install in the folder containing package.json. If the output looks old, rebuild before running. Read the first compiler diagnostic and check the indicated field or argument. For module errors, preserve the .js extensions in imports. Browser examples must run through the included local server, not directly as TypeScript files.
