9.1 TypeScript 与工程结构


9.1 TypeScript 与工程结构

剥层之旅的终点站从工程底座开始:当代码量与团队一起增长,JavaScript 的动态灵活会从资产变成负债——ctx.state.user 里到底有什么、错误类带不带 status、中间件参数对不对,全靠口口相传。TypeScript 把这些契约变成编译期约束。本节覆盖三件事:Koa 的类型化改造(Context 泛型、中间件签名、错误类层次)、目录结构随规模的演进、以及生态库类型质量的甄别。

Context 泛型:让 state 与 body 都有形状

Koa 官方类型基于泛型设计,Context 接受三个类型参数——State(请求级状态)、CustomT(自定义上下文属性)、CustomR(自定义响应属性)。真正高频的是 State:

import Koa, { Context, Middleware } from 'koa'; // 声明本服务的 State 形状:与 2.3 节的 ctx.state 用法一一对应 interface AppState { reqId: string; user?: { id: string; role: 'user' | 'admin' }; start: number; } interface AppContext extends Context<AppState> {} const app = new Koa<AppState, AppContext>(); // 从此 ctx.state.user 有完整的类型提示与检查 app.use(async (ctx: AppContext, next) => { ctx.state.reqId = crypto.randomUUID(); ctx.state.start = Date.now(); await next(); if (ctx.state.user && ctx.state.user.role !== 'admin') { // 类型系统知道 role 只有两种取值,拼错编译不过 } });

改动立竿见影:ctx.state.reqId 在所有层里都有补全;把 ctx.state.user.id 写成 ctx.state.user.idd 直接编译失败——运行时的"undefined is not a function"提前到编译期。中间件工厂同样类型化(3.3 节的 rateLimit 升级版):

interface RateLimitOptions { windowMs?: number; max: number; // 必填:编译期强制 keyGen?: (ctx: AppContext) => string; } export function rateLimit(options: RateLimitOptions): Middleware<AppState, AppContext> { const { windowMs = 60_000, keyGen = (ctx) => ctx.ip } = options; // options.max 缺失在编译期就报错,3.3 节的运行时校验退居二线 return async (ctx, next) => { /* 同第 3 章实现 */ }; }

错误类层次的类型化顺理成章——HttpError 基类带 status 与 code 字段,兜底层 instanceof 判断后的 err.status 访问才有类型保障(2.4 节的异常体系在 TS 下的完整形态):

class HttpError extends Error { constructor( public status: number, public code: string, message: string, public expose: boolean = true, ) { super(message); } } class Unauthorized extends HttpError { constructor(message = '未认证') { super(401, 'UNAUTHORIZED', message); } }

目录结构:随规模演进的三段式

1.3 节的最小约定撑到十几个资源没问题,规模化要再进一步。三段式演进路径:

阶段一(单包):routes、middleware、services 三目录,资源文件平铺。适合到万行级。

阶段二(按域分包):业务域成为一等目录——

src/ ├── app.ts 入口:只装配 ├── middleware/ 横切层:日志、鉴权、限流、错误 ├── domains/ 业务域 │ ├── order/ │ │ ├── routes.ts 该域路由(4.1 的 Router 实例) │ │ ├── service.ts 业务逻辑 │ │ └── repo.ts 数据访问 │ └── user/ ├── shared/ 错误类、校验器、缓存工具

按域分包的核心收益是依赖方向清晰:域内文件互相依赖、域间只通过 service 接口调用、middleware 与 shared 不依赖任何域——依赖图无环,重构半径可预期。

阶段三(拆服务):当不同域的发布节奏、扩容需求出现明显分化(订单要大内存、鉴权要低延迟),按 9.2 节的微服务路径拆仓。判断信号是组织信号而非技术信号:两个团队频繁改同一个仓库、发布互相阻塞,就是拆的时机。

图 9-1:类型化工程的分层依赖图

图 9-1:类型化工程的分层依赖图

生态库的类型质量甄别

TS 生态最大的暗坑是"有类型但类型烂"。装包前看 DefiniteTyped 标记(包名自带类型的优于另装 @types 的优于手写 any 声明的);装包后做一次"类型冒烟"——在测试里调一遍主接口,看提示与文档是否一致。遇到只有 any 的社区中间件,宁可在自己的代码里补一层包装声明(3.3 节工厂模式正好是包装的位置),也不要让 any 沿调用链扩散——一个 any 能污染整条链的类型推导

⚠️ 常见坑:tsconfig 里留着 "strict": false。渐进迁移的项目常见这个妥协,但它把 strictNullChecks(最值钱的检查)一起关了。正确姿势是 strict 全开、对存量未类型化文件单独豁免——宽松是全局的毒,豁免是局部的账。

本节要点回顾

  • Context 泛型先行:AppState 声明 state 形状,全链补全与检查立刻生效;
  • 必填配置进类型:工厂函数的 options 接口让配置错误编译期暴露;
  • 错误类带字段类型:HttpError 的 status 与 code 有声明,兜底层的判断才有保障;
  • 目录三段演进:单包、按域分包、拆服务,信号分别是行数、依赖混乱度、组织分化;
  • 类型质量要甄别:自带类型优于 @types 优于 any,包装层是补类型的正确位置。

类型护栏装好了。下一站把洋葱放进更大的宿主:微服务怎么拆、Serverless 怎么适配、实时通信怎么接。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U