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



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}`);
▶ Try it Yourself

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");
▶ Try it Yourself

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

📝 Exercises

  1. Basic Exercise (Difficulty: ⭐): Break down the project code into separate .ts files by module, configure tsconfig.json, and ensure that tsc compiles successfully.
  2. Advanced Exercise (Difficulty ⭐⭐): Add the tag feature to TodoService—add a tags: string[] property to Todo to support filtering by tags list --tag <labelName>. You’ll need to modify the type definition, the storage layer, and the CLI layer.
  3. Challenge (Difficulty: ⭐⭐⭐): Convert synchronous storage to asynchronous storage—change all methods in Storage<T> to async and return Promise<Result<T>>. Also, convert the Service and CLI layers to async/await. Handle asynchronous errors and ensure type safety.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏