课程库 › TypeScript › TypeScript 起步 › 第 7 课
项目课:类型安全的待办清单 —— 综合运用
项目课约 75 分钟
本课导读
六课的零件在这里合体:一个类型安全的待办清单——interface 建模、字面量联合管状态、收窄做防御、数组方法做统计,全部在浏览器里完成。规矩沿用项目课传统:V1 最小可跑 → V2 加功能 → V3 完整交付,每个版本先自己写、参考实现只做对照。做完它,你就有了第一段"能拿给别人看"的 TypeScript 代码。预计 75 分钟。
需求文档
一个命令行风格的待办清单(无界面,console 输出),支持—— ① 添加任务:标题 + 优先级(高/中/低,默认"中") ② 完成任务:按 id 标记完成;id 不存在时不崩溃、给出提示 ③ 清单打印:
[ ] #1 写周报(高)/[x] #2 回邮件(中)格式 ④ 统计报告:总数、未完成数、按优先级分布
类型要求(这是"类型安全"五个字的落点):任务必须有 interface;优先级必须是字面量联合(拼错编译报错);id 只读;所有函数签名完整。
V1:数据建模 + 添加
先把"任务长什么样"用类型钉死,再实现添加。建模是类型项目的第一步,也是最值钱的一步:
💡 提示
最大 id 的求法:list.length === 0 ? 0 : Math.max(...list.map((t) => t.id))。返回新数组用展开语法:[...list, newTask](把旧数组摊开再接一个新元素——"不改原数组、交新的",pandas 时代 sorted 的脾气)。
✅ V1 参考实现
type Priority = "高" | "中" | "低";
interface Task {
readonly id: number;
title: string;
priority: Priority;
done: boolean;
}
function addTask(list: Task[], title: string, priority: Priority = "中"): Task[] {
const maxId = list.length === 0 ? 0 : Math.max(...list.map((t) => t.id));
const newTask: Task = { id: maxId + 1, title: title, priority: priority, done: false };
return [...list, newTask];
}
let list: Task[] = [];
list = addTask(list, "写周报", "高");
list = addTask(list, "回邮件");
console.log(list);
验一下类型安全的成色:试着 addTask(list, "遛狗", "最高")——TS2345,不存在的优先级进不来;试着改 list[0].id = 99——TS2540,身份锁定。这些"试着搞破坏"的动作,本身就是项目验收的一部分。
V2:完成任务 + 防御
finishTask(list, id):把对应任务标记完成。难点在防御——id 不存在怎么办?设计:返回 Task[] | undefined(找不到就返回 undefined,调用方收窄处理):
💡 提示
list.find((t) => t.id === id) 的返回类型天生就是 Task | undefined——标准库早用联合类型把"可能找不到"写进了签名。换新对象的写法:{ ...t, done: true }(展开旧对象、覆盖一个字段)。
✅ V2 参考实现
type Priority = "高" | "中" | "低";
interface Task {
readonly id: number;
title: string;
priority: Priority;
done: boolean;
}
function finishTask(list: Task[], id: number): Task[] | undefined {
const target = list.find((t) => t.id === id);
if (target === undefined) {
return undefined;
}
return list.map((t) => (t.id === id ? { ...t, done: true } : t));
}
设计品鉴:返回 Task[] | undefined 逼着每个调用方都先收窄再使用——"忘了处理找不到的情况"这类 bug 被类型系统变成不可能。这就是联合类型做防御的实战形态。
V3:打印与统计 —— 完整交付
最后补上展示层:render(list) 逐行打印,report(list) 输出统计。然后把全部零件串成一个完整可跑的程序:
✅ V3 参考实现
type Priority = "高" | "中" | "低";
interface Task {
readonly id: number;
title: string;
priority: Priority;
done: boolean;
}
function render(list: Task[]): void {
list.map((t) => {
const mark = t.done ? "[x]" : "[ ]";
return mark + " #" + t.id + " " + t.title + "(" + t.priority + ")";
}).forEach((line) => console.log(line));
}
function report(list: Task[]): string {
const pending = list.filter((t) => !t.done).length;
const count = (p: Priority): number => list.filter((t) => t.priority === p).length;
return "共 " + list.length + " 项,未完成 " + pending + " 项" +
"(高:" + count("高") + " 中:" + count("中") + " 低:" + count("低") + ")";
}
count 是函数里的小箭头函数——注意它的参数标了 Priority,所以 count("最高") 连在函数体里都写不出来。类型合同深入到了每个角落。
验收清单
魔改方向(选做):加 removeTask(练 filter);加"按优先级排序打印"(高在前——提示:可用 ["高", "中", "低"].indexOf(t.priority) 当排序权重);把整套函数搬进本地 VS Code,体验编辑器里的类型提示补全。
📝 随堂测验
1. priority 用字面量联合而不是 string,在这个项目里换来了什么?
2. finishTask 返回 Task[] | undefined 的设计意图是?
3. addTask 返回 [...list, newTask] 而不是 list.push(newTask),风格上属于?
4. { ...t, done: true } 这个写法在做什么?
5. “验收清单里有三条破坏测试”体现了类型项目的什么观念?
本课小结
- 六课零件合体:interface 建模 + 字面量联合管状态 +
| undefined做防御 + 收窄处理 + map/filter/find 干活 - 两个新姿势:展开语法
[...list, x]/{ ...t, done: true }——"交新的不改旧的"数据纪律 - 类型安全的验收观:正确的能跑 + 错误的写不出来——破坏测试也是测试
- 你的第一段可展示 TS 代码到手(可搬进本地体验编辑器补全)
最后一课:单元测验 15 题——TypeScript 起步单元收官,也请你对这个试点单元本身提提意见。
划选正文任意文字可高亮、批注或加入复习卡
讨论
载入中…