TypeScript 综合案例:任务管理器
本文通过一个简单的任务管理器案例,把前面学习的枚举、接口、类、封装、泛型和严格类型检查串联起来。
一、案例目标
实现以下功能:
- 创建任务。
- 查询任务。
- 完成任务。
- 保存任务列表。
- 使用泛型编写通用仓库。
- 使用接口约束数据结构。
- 使用类封装业务逻辑。
二、项目结构
为了方便学习,先把代码写在一个文件中:
ts-task/ ├── src/ │ └── index.ts ├── dist/ ├── package.json └── tsconfig.json实际项目中,可以进一步拆分为:
src/ ├── models/ │ └── task.ts ├── repositories/ │ └── memory-repository.ts ├── services/ │ └── task-service.ts └── index.ts三、定义任务状态
任务状态是有限的几个固定值,适合使用字符串枚举:
enumTaskStatus{Todo='todo',Doing='doing',Done='done'}使用枚举后,任务状态不会被随意写成任意字符串:
conststatus:TaskStatus=TaskStatus.Todo;四、定义任务接口
接口用于描述任务对象的结构:
interfaceTask{readonlyid:number;title:string;status:TaskStatus;}id是任务唯一标识,创建后不应该被修改,所以使用readonly。
五、定义泛型仓库接口
不同业务可能需要保存不同类型的数据,因此仓库可以使用泛型:
interfaceRepository<Textends{readonlyid:number}>{add(item:T):void;getById(id:number):T|undefined;list():T[];}这里的泛型约束表示:传入仓库的数据必须具有只读的id数字属性。
六、实现内存仓库
使用类实现仓库接口:
classMemoryRepository<Textends{readonlyid:number}>implementsRepository<T>{privateitems:T[]=[];add(item:T):void{this.items.push(item);}getById(id:number):T|undefined{returnthis.items.find((item)=>item.id===id);}list():T[]{return[...this.items];}}这里体现了几个知识点:
private:外部不能直接修改内部数组。- 泛型
T:仓库可以保存不同类型的数据。 implements:类必须实现接口中声明的方法。T | undefined:查询不到数据时明确表示可能没有结果。
七、封装任务服务
任务服务负责处理任务相关业务:
classTaskService{constructor(privatereadonlyrepository:Repository<Task>){}create(title:string):Task{consttask:Task={id:Date.now(),title,status:TaskStatus.Todo};this.repository.add(task);returntask;}complete(id:number):boolean{consttask=this.repository.getById(id);if(!task){returnfalse;}task.status=TaskStatus.Done;returntrue;}list():Task[]{returnthis.repository.list();}}TaskService不需要知道任务具体保存在哪里,它只依赖Repository<Task>接口。这种设计方便以后把内存仓库替换成数据库仓库或接口仓库。
八、运行案例
constrepository=newMemoryRepository<Task>();consttaskService=newTaskService(repository);consttask=taskService.create('学习 TypeScript');console.log('创建任务:',task);taskService.complete(task.id);console.log('任务列表:',taskService.list());预期结果类似:
创建任务: { id: 1690000000000, title: '学习 TypeScript', status: 'todo' } 任务列表: [ { id: 1690000000000, title: '学习 TypeScript', status: 'done' } ]Date.now()的具体值会根据运行时间变化,上面的数字只是示例。
九、处理未知输入
如果任务状态来自用户输入或网络请求,不能直接断言为TaskStatus,应该先校验:
functionisTaskStatus(value:unknown):valueisTaskStatus{return(value===TaskStatus.Todo||value===TaskStatus.Doing||value===TaskStatus.Done);}functionparseTaskStatus(value:unknown):TaskStatus{if(isTaskStatus(value)){returnvalue;}returnTaskStatus.Todo;}这里使用了:
unknown:表示外部传入的数据类型未知。- 类型谓词
value is TaskStatus:告诉编译器检查成功后,值就是任务状态。 - 默认值:输入不合法时回退到
Todo。
十、完整 index.ts
下面的代码可以直接保存为src/index.ts:
enumTaskStatus{Todo='todo',Doing='doing',Done='done'}interfaceTask{readonlyid:number;title:string;status:TaskStatus;}interfaceRepository<Textends{readonlyid:number}>{add(item:T):void;getById(id:number):T|undefined;list():T[];}classMemoryRepository<Textends{readonlyid:number}>implementsRepository<T>{privateitems:T[]=[];add(item:T):void{this.items.push(item);}getById(id:number):T|undefined{returnthis.items.find((item)=>item.id===id);}list():T[]{return[...this.items];}}classTaskService{constructor(privatereadonlyrepository:Repository<Task>){}create(title:string):Task{consttask:Task={id:Date.now(),title,status:TaskStatus.Todo};this.repository.add(task);returntask;}complete(id:number):boolean{consttask=this.repository.getById(id);if(!task){returnfalse;}task.status=TaskStatus.Done;returntrue;}list():Task[]{returnthis.repository.list();}}constrepository=newMemoryRepository<Task>();consttaskService=newTaskService(repository);consttask=taskService.create('学习 TypeScript');console.log('创建任务:',task);taskService.complete(task.id);console.log('任务列表:',taskService.list());十一、配置 tsconfig.json
{"compilerOptions":{"target":"ES2015","module":"CommonJS","rootDir":"src","outDir":"dist","strict":true,"noEmitOnError":true},"include":["src/**/*.ts"],"exclude":["node_modules","dist"]}十二、运行项目
初始化项目并安装 TypeScript:
npminit-ynpminstall--save-dev typescript编译:
npx tsc运行编译结果:
nodedist/index.js也可以在package.json中添加:
{"scripts":{"build":"tsc","start":"node dist/index.js","type-check":"tsc --noEmit"}}十三、案例中的知识点对应关系
| 知识点 | 案例中的使用位置 |
|---|---|
| 枚举 | TaskStatus |
| 接口 | Task、Repository<T> |
readonly | 任务id |
| 类 | MemoryRepository、TaskService |
private | 仓库内部的items |
| 泛型 | Repository<T>和MemoryRepository<T> |
| 泛型约束 | T extends { readonly id: number } |
unknown | 外部任务状态校验 |
| 严格检查 | strict: true |
十四、可以继续扩展的功能
在这个案例上可以继续添加:
- 删除任务。
- 修改任务标题。
- 按状态筛选任务。
- 使用抽象类定义不同类型的仓库。
- 使用 Webpack 打包项目。
- 把内存仓库替换成数据库或 HTTP API。
- 为任务服务增加单元测试。
十五、本章小结
- 接口负责描述结构。
- 枚举负责约束有限状态。
- 类负责封装数据和业务行为。
- 泛型负责编写可复用的类型安全仓库。
unknown和类型谓词负责处理外部不可信输入。tsconfig.json负责统一项目编译配置。