TypeScript: TypeScript 综合项目
最后更新:2026-08-26
学完了前面29课,是时候把所有知识融会贯通——本课用 TypeScript 从零实现一个 CLI Todo 应用,涵盖类型设计、泛型、模块化、错误处理等核心知识。
1. 项目概述
(1) 功能需求
TEXT
📖 仅展示
Todo CLI 工具
├── 添加任务(add)
├── 列出任务(list)
├── 完成任务(done)
├── 删除任务(delete)
├── 按状态筛选(list --done / list --pending)
└── 数据持久化(JSON 文件存储)
(2) 技术要求
- 纯 TypeScript,无第三方依赖
- 模块化设计——类型、存储、业务逻辑、CLI 分层
- 完整的类型安全——无 any
- Result 模式处理错误
- 泛型存储层可复用
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 层完全复用。这就是分层架构的好处——展示层可替换,业务逻辑不受影响。
📖 小节
- 类型设计先行——定义 Todo、CreateTodo、UpdateTodo、Result 等核心类型
- 用工具类型(Omit、Partial)从 Todo 派生变体,遵循 DRY 原则
- 泛型存储层
Storage<T extends { id: string }>可复用于任何实体 - Result 模式让错误处理显式化——函数签名声明"可能失败",调用者必须处理
- 分层架构:类型层 → 存储层 → 服务层 → 展示层——各层职责清晰、互不耦合
- 综合运用了29课中的大部分知识点——从基础类型到泛型到错误处理
📝 作业
- 基础题(难度⭐):把项目代码按模块拆分为独立的 .ts 文件,配置 tsconfig.json,确保
tsc编译通过。 - 进阶题(难度⭐⭐):给 TodoService 添加
tag功能——Todo 增加 tags: string[] 属性,支持按标签筛选list --tag <标签名>。需要修改类型定义、存储层和 CLI 层。 - 挑战题(难度⭐⭐⭐):把同步存储改为异步存储——
Storage<T>的所有方法改为 async,返回Promise<Result<T>>。同时把 Service 和 CLI 层也改为 async/await。处理异步错误,确保类型安全。