如何实现自动为任意层级嵌套节点的叶子节点强类型校验的createContract工具函数
如何实现自动为任意层级嵌套节点的叶子节点强类型校验的createContract工具函数
我完全理解你的痛点——手动给每个叶子节点套endpoint函数确实繁琐,尤其是接口定义嵌套较深的时候,开发体验大打折扣。咱们可以通过递归类型约束和顶层工具函数来实现你想要的效果,自动为任意层级的嵌套对象的叶子节点应用强类型校验,完全不需要手动包装每个节点。
核心思路
我们需要实现两个关键部分:
- 一个递归类型,用来遍历输入对象的所有层级,将最终的叶子节点强制约束为
Endpoint类型(保留你原来的Endpoint和ExtractParams逻辑,因为它们已经完美处理了pathParams的必填/禁止规则); - 一个顶层的
createContract函数,用来接收这个递归约束的对象,在类型层面确保所有节点都符合要求,同时返回原结构。
完整实现代码
首先保留你原来的基础类型(ExtractParams和Endpoint),然后添加递归类型和工具函数:
import { z } from 'zod'; // --- 提取路径参数的类型(保留原逻辑)--- type ExtractParams<Path extends string> = Path extends `${infer _Start}:${infer Param}/${infer Rest}` ? { [K in Param]: string } & ExtractParams<`/${Rest}`> : Path extends `${infer _Start}:${infer Param}` ? { [K in Param]: string } : {}; // --- Endpoint 核心类型(保留原逻辑)--- type Endpoint< TPath extends string, TMethod extends string, TQuery = unknown, TBody = unknown, TResponse = unknown > = & { method: TMethod; path: TPath; query?: z.ZodSchema<TQuery>; body?: z.ZodSchema<TBody>; response: z.ZodSchema<TResponse>; } & (keyof ExtractParams<TPath> extends never ? { pathParams?: never } : { pathParams: z.ZodSchema<ExtractParams<TPath>> }); // --- 递归约束:处理任意层级的嵌套对象 --- type Contract<T> = { [K in keyof T]: T[K] extends object ? // 判断当前节点是否是叶子节点(具备Endpoint的核心字段method和path) T[K] extends { method: string; path: string } ? Endpoint< T[K]['path'], T[K]['method'], T[K]['query'] extends z.ZodSchema<infer Q> ? Q : unknown, T[K]['body'] extends z.ZodSchema<infer B> ? B : unknown, T[K]['response'] extends z.ZodSchema<infer R> ? R : unknown > : // 非叶子节点,继续递归处理下一层级 Contract<T[K]> : // 非对象类型直接标记为错误(仅允许嵌套对象或Endpoint叶子节点) never; }; // --- 顶层工具函数:自动应用递归类型约束 --- function createContract<T>(contract: Contract<T>): Contract<T> { return contract; }
使用示例
现在你可以直接用createContract包裹整个接口定义对象,不需要手动写endpoint函数,类型校验效果和原来完全一致:
const usersContract = createContract({ getUser: { method: 'GET', path: '/users/:id', response: z.object({ id: z.string(), name: z.string() }), // ❌ 报错:path包含参数,必须提供pathParams // pathParams: z.object({ id: z.string() }), // ✅ 加上后错误消失 }, getUsers: { method: 'GET', path: '/users', response: z.array(z.object({ id: z.string(), name: z.string() })), // ❌ 报错:path无参数,禁止提供pathParams // pathParams: z.object({ asdkm: z.string() }), }, updateUser: { // 支持任意层级嵌套 location: { method: 'PUT', path: '/users/:id/location/:locationId', body: z.object({ name: z.string() }), response: z.object({ abra: z.array(z.string()), kadabra: z.number() }), // ❌ 报错:path包含两个参数,必须提供pathParams // pathParams: z.object({ id: z.string(), locationId: z.string() }), // ✅ 正确写法 }, // 继续嵌套也没问题 profile: { method: 'PATCH', path: '/users/:id/profile', body: z.object({ avatar: z.string() }), response: z.object({ success: z.boolean() }), pathParams: z.object({ id: z.string() }), // ✅ 符合要求 }, }, getUsers2: { method: 'GET', path: '/users', pathParams: z.object({ asdkm: z.string() }), // ❌ 报错:path无参数,禁止pathParams response: z.array(z.object({ id: z.string(), name: z.string() })), }, });
关键说明
递归类型
Contract<T>:- 遍历输入对象的每个属性,如果是对象,先判断是否是
Endpoint结构(有method和path); - 如果是叶子节点,直接应用原有的
Endpoint类型约束,自动推导path和method的泛型,进而触发pathParams的必填/禁止规则; - 如果是非叶子节点(嵌套对象),则继续递归处理下一层级;
- 非对象类型直接标记为
never,确保只有合法的嵌套结构或Endpoint叶子节点能通过校验。
- 遍历输入对象的每个属性,如果是对象,先判断是否是
createContract函数:- 不需要复杂的泛型推导,只需要接收
Contract<T>类型的参数并返回相同类型; - 函数本身只是透传输入对象,核心作用是在编译时触发类型校验。
- 不需要复杂的泛型推导,只需要接收
效果对比
- 原来的方式:每个叶子节点都要手动写
endpoint(...),嵌套越深越繁琐; - 新方式:只需要在顶层套一次
createContract(...),所有层级的叶子节点自动应用强类型校验,开发体验大幅提升。
内容来源于stack exchange
相关产品推荐
相关产品推荐

