TypeScript泛型约束问题:如何将Zod Schema解析结果传入函数参数
解决Express控制器包装器的TypeScript类型错误
我正在构建自定义Express控制器函数来减少代码冗余,所有处理函数都重复包含try-catch封装、Zod Schema解析,以及将请求的body、query、params合并为单个对象的逻辑,因此想用包装器统一处理这些操作,同时保证类型安全。
原实现代码
function controller< T extends B & Q & P, B extends Record<string, any>, Q extends Record<string, any>, P extends Record<string, any> >(handlerFn: (args: T) => Promise<any>, zodSchema: { parse: (data: unknown) => { body: B; query: Q; params: P } }) { return async (req: express.Request, res: express.Response, next: express.NextFunction) => { try { const r = zodSchema.parse(req); const args = { ...r.body, ...r.params, ...r.query }; await handlerFn(args); } catch (e) { next(e); } }; }
遇到的类型错误
Argument of type 'B & P & Q' is not assignable to parameter of type 'T'. 'B & P & Q' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Record<string, any>'.
问题原因
泛型参数T被定义为B & Q & P的子类型,但合并后的args本质是B & Q & P,TypeScript无法保证它和T完全一致——因为T可以是B & Q & P的更具体子类型,导致类型不兼容。
解决方案
方案1:直接验证合并后的参数(推荐)
简化泛型逻辑,让Zod Schema直接验证合并后的请求数据,泛型T由Schema自动推导:
import { z } from "zod"; import express from "express"; function controller<T>( handlerFn: (args: T) => Promise<any>, zodSchema: z.ZodType<T> ) { return async (req: express.Request, res: express.Response, next: express.NextFunction) => { try { // 合并req的body、query、params后用Schema验证 const combinedData = { ...req.body, ...req.params, ...req.query }; const args = zodSchema.parse(combinedData); await handlerFn(args); } catch (e) { next(e); } }; }
使用示例:
// 定义合并后的参数Schema const UserControllerSchema = z.object({ id: z.coerce.number(), // 来自params name: z.string(), // 来自body page: z.coerce.number().optional() // 来自query }); // 创建控制器 const getUserController = controller(async (args) => { // args自动拥有id、name、page的类型提示 console.log(args.id, args.name, args.page); }, UserControllerSchema);
方案2:保留拆分验证逻辑
如果需要单独验证body、query、params,调整泛型定义,让T直接等于三者的交集,而非子类型:
import express from "express"; function controller< B extends Record<string, any>, Q extends Record<string, any>, P extends Record<string, any>, T = B & Q & P // 直接将T定义为三者的交集类型 >(handlerFn: (args: T) => Promise<any>, zodSchema: { parse: (data: unknown) => { body: B; query: Q; params: P } }) { return async (req: express.Request, res: express.Response, next: express.NextFunction) => { try { const r = zodSchema.parse(req); const args = { ...r.body, ...r.params, ...r.query } as T; await handlerFn(args); } catch (e) { next(e); } }; }
方案3:Zod拆分后合并Schema
如果希望单独定义body、query、params的Schema,再合并为完整参数类型:
import { z } from "zod"; import express from "express"; // 单独定义各部分Schema const BodySchema = z.object({ name: z.string() }); const QuerySchema = z.object({ page: z.coerce.number().optional() }); const ParamsSchema = z.object({ id: z.coerce.number() }); // 合并为完整参数Schema const CombinedSchema = BodySchema.merge(QuerySchema).merge(ParamsSchema); // 使用包装器 const updateUserController = controller(async (args) => { console.log(args.id, args.name, args.page); }, CombinedSchema);
内容的提问来源于stack exchange,提问作者Ahmet Yazıcı
相关产品推荐
相关产品推荐

