TypeScript: TypeScript Comprehensive Project
Last updated: 2026-08-26
Now that you’ve completed the first 29 lessons, it’s time to bring all that knowledge together—in this lesson, we’ll build a CLI Todo app from scratch using TypeScript, covering core concepts such as type design, generics, modularity, and error handling.
1. Project Overview
(1) Functional Requirements
TEXT
📖 Display only
Todo CLI Tools
├── Add a Task(add)
├── List Tasks(list)
├── Complete the task(done)
├── Delete Task(delete)
├── Filter by Status(list --done / list --pending)
└── Data Persistence(JSON File Storage)
(2) Technical Requirements
- Pure TypeScript, no third-party dependencies
- Modular Design—Layering of Types, Storage, Business Logic, and CLI
- Full type safety—no
any - Handling Errors in Result Mode
- Reusable generic storage layer
2. Type Design
(1) Core Type Definitions
TYPESCRIPT
// types.ts
type Priority = "low" | "medium" | "high";
type TodoStatus = "pending" | "done";
interface Todo {
id: string;
title: string;
description?: string;
priority: Priority;
status: TodoStatus;
createdAt: string;
updatedAt: string;
}
// Derivation Using Tool Types——DRY Principles
type CreateTodo = Omit<Todo, "id" | "createdAt" | "updatedAt">;
type UpdateTodo = Partial<Omit<Todo, "id" | "createdAt">>;
interface TodoFilter {
status?: TodoStatus;
priority?: Priority;
}
type SortBy = "createdAt" | "priority" | "title";
type SortOrder = "asc" | "desc";
interface TodoSort {
by: SortBy;
order: SortOrder;
}
(2) Result Pattern
TYPESCRIPT
// result.ts
type Success<T> = { ok: true; value: T };
type Failure<E> = { ok: false; error: E };
type Result<T, E = string> = Success<T> | Failure<E>;
function ok<T>(value: T): Success<T> {
return { ok: true, value };
}
function err<E>(error: E): Failure<E> {
return { ok: false, error };
}
3. Generic Storage Layer
(1) Storage Interface
TYPESCRIPT
// storage.ts
interface Storage<T extends { id: string }> {
getAll(): Result<T[]>;
getById(id: string): Result<T | null>;
save(item: T): Result<T>;
delete(id: string): Result<boolean>;
find(predicate: (item: T) => boolean): Result<T[]>;
}
(2) JSON File Storage Implementation
TYPESCRIPT
class JsonFileStorage<T extends { id: string }> implements Storage<T> {
private data: Map<string, T> = new Map();
private loaded: boolean = false;
constructor(private filePath: string) {}
private load(): Result<void> {
if (this.loaded) return ok(undefined);
try {
let fs = require("fs");
if (fs.existsSync(this.filePath)) {
let raw = fs.readFileSync(this.filePath, "utf-8");
let items: T[] = JSON.parse(raw);
items.forEach(item => this.data.set(item.id, item));
}
this.loaded = true;
return ok(undefined);
} catch (error) {
return err(`Failed to load:${error instanceof Error ? error.message : String(error)}`);
}
}
private persist(): Result<void> {
try {
let fs = require("fs");
let items = Array.from(this.data.values());
fs.writeFileSync(this.filePath, JSON.stringify(items, null, 2), "utf-8");
return ok(undefined);
} catch (error) {
return err(`Save Failed:${error instanceof Error ? error.message : String(error)}`);
}
}
getAll(): Result<T[]> {
let r = this.load();
if (!r.ok) return err(r.error);
return ok(Array.from(this.data.values()));
}
getById(id: string): Result<T | null> {
let r = this.load();
if (!r.ok) return err(r.error);
return ok(this.data.get(id) ?? null);
}
save(item: T): Result<T> {
let r = this.load();
if (!r.ok) return err(r.error);
this.data.set(item.id, { ...item, updatedAt: new Date().toISOString() } as T);
let p = this.persist();
if (!p.ok) return err(p.error);
return ok({ ...item, updatedAt: new Date().toISOString() } as T);
}
delete(id: string): Result<boolean> {
let r = this.load();
if (!r.ok) return err(r.error);
let existed = this.data.delete(id);
let p = this.persist();
if (!p.ok) return err(p.error);
return ok(existed);
}
find(predicate: (item: T) => boolean): Result<T[]> {
let r = this.getAll();
if (!r.ok) return err(r.error);
return ok(r.value.filter(predicate));
}
}
4. Business Logic Layer
TYPESCRIPT
// service.ts
class TodoService {
private idCounter: number = 0;
constructor(private storage: Storage<Todo>) {}
private generateId(): string {
return `todo_${Date.now()}_${++this.idCounter}`;
}
add(input: CreateTodo): Result<Todo> {
let now = new Date().toISOString();
let todo: Todo = {
id: this.generateId(),
title: input.title,
description: input.description,
priority: input.priority,
status: "pending",
createdAt: now,
updatedAt: now
};
return this.storage.save(todo);
}
list(filter?: TodoFilter, sort?: TodoSort): Result<Todo[]> {
let r = this.storage.getAll();
if (!r.ok) return err(r.error);
let todos = r.value;
// Filter
if (filter?.status) todos = todos.filter(t => t.status === filter.status);
if (filter?.priority) todos = todos.filter(t => t.priority === filter.priority);
// Sort
let sortBy: SortBy = sort?.by ?? "createdAt";
let order: SortOrder = sort?.order ?? "desc";
let priorityOrder: Record<Priority, number> = { high: 3, medium: 2, low: 1 };
todos.sort((a, b) => {
let cmp = 0;
if (sortBy === "createdAt") cmp = a.createdAt.localeCompare(b.createdAt);
else if (sortBy === "priority") cmp = priorityOrder[a.priority] - priorityOrder[b.priority];
else if (sortBy === "title") cmp = a.title.localeCompare(b.title);
return order === "asc" ? cmp : -cmp;
});
return ok(todos);
}
done(id: string): Result<Todo> {
let r = this.storage.getById(id);
if (!r.ok) return err(r.error);
if (!r.value) return err(`Task ${id} Does not exist`);
if (r.value.status === "done") return err(`Task ${id} Completed`);
return this.storage.save({ ...r.value, status: "done" });
}
delete(id: string): Result<boolean> {
return this.storage.delete(id);
}
update(id: string, updates: UpdateTodo): Result<Todo> {
let r = this.storage.getById(id);
if (!r.ok) return err(r.error);
if (!r.value) return err(`Task ${id} Does not exist`);
return this.storage.save({ ...r.value, ...updates });
}
}
▶ Example: Service-layer testing
Output:
TEXT
📖 Display only
Added: Demo task
TYPESCRIPT
// Usage——Create a service and perform an operation
let storage = new JsonFileStorage<Todo>("./todos.json");
let service = new TodoService(storage);
// Add a Task
let addResult = service.add({ title: "StudyTypeScript", priority: "high" });
if (addResult.ok) {
console.log(`Added:${addResult.value.title}`);
}
// List Tasks
let listResult = service.list({ status: "pending" });
if (listResult.ok) {
console.log(`To-Do List:${listResult.value.length} items`);
listResult.value.forEach(t => console.log(` - ${t.title} [${t.priority}]`));
}
// Complete the task
if (addResult.ok) {
let doneResult = service.done(addResult.value.id);
if (doneResult.ok) {
console.log(`Completed:${doneResult.value.title}`);
}
}
Output:
TEXT
📖 Display only
[LOG] Executing: list
[LOG] Found 0 items
5. CLI Presentation Layer
TYPESCRIPT
// cli.ts
class TodoCLI {
constructor(private service: TodoService) {}
run(command: string, args: string[]): void {
switch (command) {
case "add": this.handleAdd(args); break;
case "list": this.handleList(args); break;
case "done": this.handleDone(args); break;
case "delete": this.handleDelete(args); break;
default: this.printHelp();
}
}
private handleAdd(args: string[]): void {
if (args.length === 0) {
console.log("Usage:add <Title> [Priority:low|medium|high]");
return;
}
let title = args[0];
let priority: Priority =
args[1] === "low" || args[1] === "medium" || args[1] === "high"
? args[1] : "medium";
let r = this.service.add({ title, priority });
if (r.ok) {
console.log(`✅ Added:${r.value.title} [${r.value.priority}]`);
} else {
console.log(`❌ Failed to add:${r.error}`);
}
}
private handleList(args: string[]): void {
let filter: TodoFilter = {};
if (args.includes("--done")) filter.status = "done";
if (args.includes("--pending")) filter.status = "pending";
let r = this.service.list(filter);
if (!r.ok) { console.log(`❌ Query Failed:${r.error}`); return; }
if (r.value.length === 0) { console.log("📭 No tasks"); return; }
console.log("\n📋 Task List:");
console.log("─".repeat(50));
for (let t of r.value) {
let s = t.status === "done" ? "✅" : "⬜";
let p = { low: "🟢", medium: "🟡", high: "🔴" }[t.priority];
console.log(`${s} ${p} [${t.id}] ${t.title}`);
}
console.log("─".repeat(50));
console.log(`Total ${r.value.length} task`);
}
private handleDone(args: string[]): void {
if (args.length === 0) { console.log("Usage:done <TaskID>"); return; }
let r = this.service.done(args[0]);
if (r.ok) console.log(`✅ Completed:${r.value.title}`);
else console.log(`❌ Operation Failed:${r.error}`);
}
private handleDelete(args: string[]): void {
if (args.length === 0) { console.log("Usage:delete <TaskID>"); return; }
let r = this.service.delete(args[0]);
if (r.ok && r.value) console.log("🗑️ Deleted");
else if (r.ok) console.log("❌ The task does not exist.");
else console.log(`❌ Deletion Failed:${r.error}`);
}
private printHelp(): void {
console.log("Todo CLI Tools:");
console.log(" add <Title> [Priority] Add a Task");
console.log(" list [--done|--pending] List Tasks");
console.log(" done <ID> Complete the task");
console.log(" delete <ID> Delete Task");
}
}
6. Input Files
TYPESCRIPT
// index.ts
import { TodoService } from "./service";
import { JsonFileStorage } from "./storage";
import { TodoCLI } from "./cli";
import type { Todo } from "./types";
let storage = new JsonFileStorage<Todo>("./todos.json");
let service = new TodoService(storage);
let cli = new TodoCLI(service);
let args = process.argv.slice(2);
let command = args[0] ?? "list";
let commandArgs = args.slice(1);
cli.run(command, commandArgs);
7. Project Knowledge List
The concepts covered in this project correspond to the following previous lessons:
| Key Concepts | Corresponding Course | Application in Projects |
|---|---|---|
| Primitive Types | Lesson 4 | Todo's string/boolean Types |
| Type Inference | Lesson 5 | Type Annotations for Omitted Variables |
| Union Types | Lesson 6 | Priority, TodoStatus |
| Arrays | Lesson 7 | Storing in Todo[] |
| Object Type | Lesson 8 | Todo and TodoFilter Interfaces |
| Interface | Lesson 9 | Storage Interface Definition |
| Function Types | Lesson 10 | Predicate Callback Parameters |
| Type Aliases | Lesson 11 | Result Type |
| Class | Lesson 13 | TodoService, JsonFileStorage |
| Classes and Interfaces | Lesson 14 | implements Storage |
| Generics | Lesson 15 | Adding Generic Parameters to Storage |
| Generic Constraints | Lesson 16 | T extends { id: string } |
| Type Guards | Lesson 17 | result.ok Narrowing |
| Type Assertions | Lesson 18 | as T Assertions |
| Tool Type | Lesson 19 | Omit, Partial, Record |
| Modules | Lesson 22 | Import/Export Modules |
| tsconfig | Lesson 24 | Project Configuration |
| Error Handling | Lesson 26 | Result Pattern |
▶ Example: In-Memory Storage Implementation
TYPESCRIPT
class MemoryStorage<T extends { id: string }> implements Storage<T> {
private data: Map<string, T> = new Map();
getAll(): Result<T[]> {
return ok(Array.from(this.data.values()));
}
getById(id: string): Result<T | null> {
return ok(this.data.get(id) ?? null);
}
save(item: T): Result<T> {
this.data.set(item.id, item);
return ok(item);
}
delete(id: string): Result<boolean> {
return ok(this.data.delete(id));
}
find(predicate: (item: T) => boolean): Result<T[]> {
let all = Array.from(this.data.values());
return ok(all.filter(predicate));
}
}
let memStore = new MemoryStorage<Todo>();
let memService = new TodoService(memStore);
let r = memService.add({ title: "Demo task", priority: "high" });
if (r.ok) console.log(`Added: ${r.value.title}`);
Output:
TEXT
📖 Display only
Added: Demo task
▶ Example: Layered Architecture with Dependency Injection
TYPESCRIPT
interface Logger {
log(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
}
class SilentLogger implements Logger {
log(_message: string): void { }
}
class App {
constructor(
private service: TodoService,
private logger: Logger
) {}
run(command: string): void {
this.logger.log(`Executing: ${command}`);
let result = this.service.list();
if (result.ok) {
this.logger.log(`Found ${result.value.length} items`);
}
}
}
let app = new App(
new TodoService(new MemoryStorage<Todo>()),
new ConsoleLogger()
);
app.run("list");
Output:
TEXT
📖 Display only
[LOG] Executing: list
[LOG] Found 0 items
❓ FAQ
Q What features can be added to this project?
A Suggested areas for expansion: (1) Asynchronous storage—switch to
async/await (2) Tag system—add string[] tags to tasks (3) Subtasks—todes can have children (4) Export to CSV/Markdown (5) Support for multiple lists. Each extension is a great exercise for reinforcing what you’ve learned in previous lessons.Q Why use the Result pattern instead of try/catch?
A In CLI tools, errors are predictable business errors (such as a task not existing). The Result pattern makes it clear in the function signature that the operation “may fail,” and the caller must handle it. It is more suitable for this scenario than try/catch—the error is not an “exception” but a “normal branch.”
Q Why does the storage layer use generics instead of directly using
Todo?A Generics make the storage layer reusable—by adding generic parameters,
JsonFileStorage can store any entity with an id property. As the project grows, there’s no need to write a new storage implementation for each entity. This is the core value of generics—write once, use everywhere.Q How do I convert a project into a Web API?
A The core architecture remains unchanged—you simply need to replace the CLI layer with an Express routing layer. The Storage and Service layers are reused entirely. This is the benefit of a layered architecture—the presentation layer can be replaced without affecting the business logic.
📖 Summary
- Start with type design—define core types such as
Todo,CreateTodo,UpdateTodo, andResult - Derive variants from Todo using tool types (Omit, Partial), following the DRY principle
- Generic storage layer
Storage<T extends { id: string }>can be reused for any entity - The Result pattern makes error handling explicit—the function signature declares that it "may fail," and the caller must handle the error.
- Layered architecture: Type layer → Storage layer → Service layer → Presentation layer—each layer has clearly defined responsibilities and is decoupled from the others
- Integrates most of the concepts covered in Lesson 29—from basic types to generics to error handling
📝 Exercises
- Basic Exercise (Difficulty: ⭐): Break down the project code into separate .ts files by module, configure tsconfig.json, and ensure that
tsccompiles successfully. - Advanced Exercise (Difficulty ⭐⭐): Add the
tagfeature to TodoService—add atags: string[]property to Todo to support filtering by tagslist --tag <labelName>. You’ll need to modify the type definition, the storage layer, and the CLI layer. - Challenge (Difficulty: ⭐⭐⭐): Convert synchronous storage to asynchronous storage—change all methods in
Storage<T>toasyncand returnPromise<Result<T>>. Also, convert the Service and CLI layers toasync/await. Handle asynchronous errors and ensure type safety.