You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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ı

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 22:15:33