TypeScript: TypeScript 综合项目

最后更新:2026-08-26

学完了前面29课,是时候把所有知识融会贯通——本课用 TypeScript 从零实现一个 CLI Todo 应用,涵盖类型设计、泛型、模块化、错误处理等核心知识。

1. 项目概述

(1) 功能需求

TEXT 📖 仅展示
Todo CLI 工具
├── 添加任务(add)
├── 列出任务(list)
├── 完成任务(done)
├── 删除任务(delete)
├── 按状态筛选(list --done / list --pending)
└── 数据持久化(JSON 文件存储)

(2) 技术要求


2. 类型设计

(1) 核心类型定义

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;
}

// 用工具类型派生——DRY 原则
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 模式

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. 泛型存储层

(1) 存储接口

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 文件存储实现

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(`加载失败:${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(`保存失败:${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);
  }

  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. 业务逻辑层

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;

    // 筛选
    if (filter?.status) todos = todos.filter(t => t.status === filter.status);
    if (filter?.priority) todos = todos.filter(t => t.priority === filter.priority);

    // 排序
    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(`任务 ${id} 不存在`);
    if (r.value.status === "done") return err(`任务 ${id} 已完成`);
    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(`任务 ${id} 不存在`);
    return this.storage.save({ ...r.value, ...updates });
  }
}

▶ 示例:服务层测试

TYPESCRIPT
// 使用——创建服务并执行操作
let storage = new JsonFileStorage<Todo>("./todos.json");
let service = new TodoService(storage);

// 添加任务
let addResult = service.add({ title: "学习TypeScript", priority: "high" });
if (addResult.ok) {
  console.log(`已添加:${addResult.value.title}`);
}

// 列出任务
let listResult = service.list({ status: "pending" });
if (listResult.ok) {
  console.log(`待办任务:${listResult.value.length} 项`);
  listResult.value.forEach(t => console.log(`  - ${t.title} [${t.priority}]`));
}

// 完成任务
if (addResult.ok) {
  let doneResult = service.done(addResult.value.id);
  if (doneResult.ok) {
    console.log(`已完成:${doneResult.value.title}`);
  }
}
▶ 试一试

5. CLI 展示层

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("用法:add <标题> [优先级: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(`✅ 已添加:${r.value.title} [${r.value.priority}]`);
    } else {
      console.log(`❌ 添加失败:${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(`❌ 查询失败:${r.error}`); return; }
    if (r.value.length === 0) { console.log("📭 没有任务"); return; }

    console.log("\n📋 任务列表:");
    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(`共 ${r.value.length} 项任务`);
  }

  private handleDone(args: string[]): void {
    if (args.length === 0) { console.log("用法:done <任务ID>"); return; }
    let r = this.service.done(args[0]);
    if (r.ok) console.log(`✅ 已完成:${r.value.title}`);
    else console.log(`❌ 操作失败:${r.error}`);
  }

  private handleDelete(args: string[]): void {
    if (args.length === 0) { console.log("用法:delete <任务ID>"); return; }
    let r = this.service.delete(args[0]);
    if (r.ok && r.value) console.log("🗑️ 已删除");
    else if (r.ok) console.log("❌ 任务不存在");
    else console.log(`❌ 删除失败:${r.error}`);
  }

  private printHelp(): void {
    console.log("Todo CLI 工具:");
    console.log("  add <标题> [优先级]       添加任务");
    console.log("  list [--done|--pending]   列出任务");
    console.log("  done <ID>                 完成任务");
    console.log("  delete <ID>               删除任务");
  }
}

6. 入口文件

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. 项目知识清单

本项目用到的知识点对应前面的课程:

知识点 对应课程 项目中的应用
基础类型 第4课 Todo 的 string/boolean 类型
类型推断 第5课 变量省略类型注解
联合类型 第6课 Priority、TodoStatus
数组 第7课 Todo[] 存储
对象类型 第8课 Todo、TodoFilter 接口
接口 第9课 Storage 接口定义
函数类型 第10课 predicate 回调参数
类型别名 第11课 Result 类型
第13课 TodoService、JsonFileStorage
类与接口 第14课 implements Storage
泛型 第15课 Storage 加泛型参数
泛型约束 第16课 T extends { id: string }
类型守卫 第17课 result.ok 收窄
类型断言 第18课 as T 断言
工具类型 第19课 Omit、Partial、Record
模块系统 第22课 import/export 模块化
tsconfig 第24课 项目配置
错误处理 第26课 Result 模式

❓ 常见问题

Q 这个项目可以扩展哪些功能?
A 建议扩展方向:(1) 异步存储——改为 async/await (2) 标签系统——给任务添加 string[] 标签 (3) 子任务——Todo 包含 children (4) 导出为 CSV/Markdown (5) 多列表支持。每个扩展都是巩固前面课程知识的好练习。
Q 为什么用 Result 模式而不是 try/catch?
A CLI 工具中错误是可预测的业务错误(任务不存在等),Result 模式让函数签名明确"可能失败",调用者必须处理。比 try/catch 更适合这种场景——错误不是"异常"而是"正常分支"。
Q 为什么存储层用泛型而不是直接用 Todo?
A 泛型让存储层可复用——JsonFileStorage 加泛型参数可以存储任何有 id 属性的实体。项目增长后不需要为每个实体写新的存储实现。这是泛型的核心价值——写一次,到处用。
Q 如何把项目改造为 Web API?
A 核心架构不变——只需把 CLI 层替换为 Express 路由层。Storage 和 Service 层完全复用。这就是分层架构的好处——展示层可替换,业务逻辑不受影响。

📖 小节

📝 作业

  1. 基础题(难度⭐):把项目代码按模块拆分为独立的 .ts 文件,配置 tsconfig.json,确保 tsc 编译通过。
  2. 进阶题(难度⭐⭐):给 TodoService 添加 tag 功能——Todo 增加 tags: string[] 属性,支持按标签筛选 list --tag <标签名>。需要修改类型定义、存储层和 CLI 层。
  3. 挑战题(难度⭐⭐⭐):把同步存储改为异步存储——Storage<T> 的所有方法改为 async,返回 Promise<Result<T>>。同时把 Service 和 CLI 层也改为 async/await。处理异步错误,确保类型安全。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

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

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