如何在TypeScript中正确为中间件与控制器链添加类型
实现TypeScript中间件链与控制器的类型约束
要解决中间件间输入输出类型匹配、以及中间件最终输出与控制器输入匹配的问题,我们可以通过TypeScript的泛型和条件类型来追踪类型流转,实现严格的编译阶段校验。
核心实现步骤
1. 定义类型辅助工具
首先需要几个辅助类型来提取中间件的输入输出类型,以及校验中间件链的合法性:
// 提取中间件的输入类型 type GetMiddlewareInput<M extends BaseMiddleware<any, any>> = M extends BaseMiddleware<infer I, any> ? I : never; // 提取中间件的输出类型 type GetMiddlewareOutput<M extends BaseMiddleware<any, any>> = M extends BaseMiddleware<any, infer O> ? O : never; // 递归校验中间件链的类型流转,返回最终的输出类型 type CheckMiddlewareChain< Chain extends BaseMiddleware<any, any>[], CurrentInput extends Record<string, any> > = Chain extends [ infer First extends BaseMiddleware<any, any>, ...infer Rest extends BaseMiddleware<any, any>[] ] ? // 检查当前中间件的输入是否能被当前传入的数据满足 GetMiddlewareInput<First> extends CurrentInput ? // 递归处理剩余中间件,更新当前输入为当前中间件的输出与原有输入的合并 CheckMiddlewareChain<Rest, CurrentInput & GetMiddlewareOutput<First>> : never // 类型不匹配时返回never,触发报错 : CurrentInput; // 处理完所有中间件后返回最终的输入(即所有中间件的输出合并)
2. 约束route函数的泛型
利用上面的辅助类型,给route函数添加严格的泛型约束:
const route = < Chain extends BaseMiddleware<any, any>[], Controller extends BaseController<any>, // 推导中间件链的最终输出类型 FinalOutput extends Record<string, any> = CheckMiddlewareChain<Chain, {}> >( path: string, middlewares: Chain, // 约束控制器的输入类型必须是中间件最终输出的子集 controller: Controller extends BaseController<infer C> ? (C extends FinalOutput ? Controller : never) : never ) => { // 实际路由逻辑:依次执行中间件,合并输出数据 let data: Record<string, any> = {}; for (const middleware of middlewares) { data = { ...data, ...middleware.index(data as any) }; } controller.index(data as any); };
完整代码示例
将上述类型和函数与原有的中间件、控制器类结合,完整代码如下:
abstract class BaseMiddleware< Input extends Record<string, any>, Output extends Record<string, any>, > { abstract index(data: Input): Output } abstract class BaseController<Input extends Record<string, any>> { abstract index(data: Input): void } class AuthMiddleware extends BaseMiddleware<{}, { userID: number }> { index(data: {}) { return { userID: 1, } } } class UserPopulateMiddleware extends BaseMiddleware<{ userID: number }, { username: string }> { index(data: { userID: number }) { return { username: `getUsernameFromUserID(${data.userID})`, } } } class AnalyticMiddleware extends BaseMiddleware<{ userID: number; username: string }, {}> { index(data: { userID: number; username: string }) { return {} } } class AuthorizedController extends BaseController<{ userID: number }> { index(data: { userID: number }) {} } class UserPopulatedController extends BaseController<{ userID: number; username: string }> { index(data: { userID: number; username: string }) {} } class Controller extends BaseController<{}> { index(data: {}) {} } // 类型辅助工具 type GetMiddlewareInput<M extends BaseMiddleware<any, any>> = M extends BaseMiddleware<infer I, any> ? I : never; type GetMiddlewareOutput<M extends BaseMiddleware<any, any>> = M extends BaseMiddleware<any, infer O> ? O : never; type CheckMiddlewareChain< Chain extends BaseMiddleware<any, any>[], CurrentInput extends Record<string, any> > = Chain extends [ infer First extends BaseMiddleware<any, any>, ...infer Rest extends BaseMiddleware<any, any>[] ] ? GetMiddlewareInput<First> extends CurrentInput ? CheckMiddlewareChain<Rest, CurrentInput & GetMiddlewareOutput<First>> : never : CurrentInput; // 带类型约束的route函数 const route = < Chain extends BaseMiddleware<any, any>[], Controller extends BaseController<any>, FinalOutput extends Record<string, any> = CheckMiddlewareChain<Chain, {}> >( path: string, middlewares: Chain, controller: Controller extends BaseController<infer C> ? (C extends FinalOutput ? Controller : never) : never ) => { let data: Record<string, any> = {}; for (const middleware of middlewares) { data = { ...data, ...middleware.index(data as any) }; } controller.index(data as any); }; // 测试用例 route('test', [new AuthMiddleware()], new Controller()) // ✅ 通过 route('test', [], new Controller()) // ✅ 通过 route('test', [new AuthMiddleware(), new UserPopulateMiddleware()], new UserPopulatedController()) // ✅ 通过 route('test', [], new UserPopulatedController()) // ❌ 报错:控制器需要的字段未被中间件提供 route('test', [new AuthMiddleware()], new UserPopulatedController()) // ❌ 报错:缺少username字段 route('test', [new UserPopulateMiddleware()], new AuthorizedController()) // ❌ 报错:第一个中间件需要userID,但初始数据为空 route('test', [], new AuthorizedController()) // ❌ 报错:缺少userID字段 route('test', [new AuthMiddleware(), new AnalyticMiddleware()], new AuthorizedController()) // ❌ 报错:AnalyticMiddleware需要username字段,但前一个中间件未提供
原理说明
CheckMiddlewareChain通过递归遍历中间件元组,确保每个中间件的输入类型能被前序中间件的输出(或初始空对象)满足,并逐步合并输出类型。route函数的泛型约束确保控制器的输入类型是中间件链最终输出类型的子集,也就是说中间件必须提供控制器所需的所有字段。- 当类型不匹配时,TypeScript会将控制器的类型推导为
never,从而在编译阶段抛出错误,避免运行时类型不兼容问题。
内容的提问来源于stack exchange,提问作者kennarddh
相关产品推荐
相关产品推荐

