三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

TypeScript总结:19、综合案例

TypeScript总结:19、综合案例

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
接口TaskRepository<T>
readonly任务id
MemoryRepositoryTaskService
private仓库内部的items
泛型Repository<T>MemoryRepository<T>
泛型约束T extends { readonly id: number }
unknown外部任务状态校验
严格检查strict: true

十四、可以继续扩展的功能

在这个案例上可以继续添加:

  1. 删除任务。
  2. 修改任务标题。
  3. 按状态筛选任务。
  4. 使用抽象类定义不同类型的仓库。
  5. 使用 Webpack 打包项目。
  6. 把内存仓库替换成数据库或 HTTP API。
  7. 为任务服务增加单元测试。

十五、本章小结

  • 接口负责描述结构。
  • 枚举负责约束有限状态。
  • 类负责封装数据和业务行为。
  • 泛型负责编写可复用的类型安全仓库。
  • unknown和类型谓词负责处理外部不可信输入。
  • tsconfig.json负责统一项目编译配置。
← 返回列表