CAPSTONE PROJECT · LESSON 22 OF 24
Capstone: a tested task domain
Build reusable task behavior before connecting it to a screen.
- domain model
- immutability
- validation
- tests
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
A domain module owns business rules independently of the browser. Our Task has an id, a title and a completion flag. createTask trims the title and rejects invalid values; callers decide how to show those errors.
Pure transformations make behavior easy to test. toggleTask returns a new array and creates a new object only for the matching task. removeTask filters by id. An unknown id leaves the contents unchanged rather than silently modifying another task.
parseTasks validates data loaded from storage. It checks every field, reuses title validation, rejects duplicate ids and constructs fresh objects. Local storage is an external boundary too: an older app version or user can change it.
Run npm run check, npm run build and npm start. The executable test program checks creation, trimming, immutability, toggling, deletion and invalid stored data. The next lesson reuses this exact domain module in a browser app.
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, toggleTask, removeTask, parseTasks } from "./tasks.js";
function assert(condition: boolean, message: string): void { if (!condition) throw new Error(message); }
const original = [createTask("1", " Learn TypeScript ")];
assert(original[0]?.title === "Learn TypeScript", "Trim title");
const changed = toggleTask(original, "1");
assert(changed[0]?.done === true, "Toggle");
assert(original[0]?.done === false, "Do not mutate input");
assert(removeTask(changed, "1").length === 0, "Remove");
assert(parseTasks(JSON.parse(JSON.stringify(changed)))[0]?.done === true, "Round trip");
let rejected = false;
try { parseTasks([{ id: "1", title: "Task", done: "yes" }]); }
catch { rejected = true; }
assert(rejected, "Reject malformed storage");
console.log("6 task checks passed");
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 };
});
}
package.json
{
"name": "edupoly-typescript-capstone-task-domain",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"check": "tsc --noEmit",
"build": "tsc",
"start": "node dist/main.js"
},
"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
- The tests import the same functions that the browser will use.
- Checking original after toggling catches accidental mutation.
- The JSON round trip exercises the boundary parser with real serialized data.
Expected result
6 task checks passed4. Practice independently
Test duplicate ids and blank titles.
Show the explanation
Call parseTasks with two valid objects sharing an id and assert that it throws. Separately call createTask("2", " ") and assert that it throws.
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.
