TypeScript递归API客户端构建中的类型推断失败问题
TypeScript递归API客户端构建中的类型推断失败问题
看起来你在构建类型安全API客户端时遇到了递归类型推断的问题——你的ClientInfer类型没有正确地从as const断言后的契约对象中提取端点类型信息,导致客户端方法失去了类型检查和自动补全能力。让我们一步步分析并解决这个问题。
问题根源分析
你的核心问题出在ClientInfer类型和buildClient函数的泛型约束上:
- 当使用
as const断言后,契约对象的类型会变成深层的只读字面量类型,但你的ClientInfer类型没有正确处理这种嵌套结构与Endpoint类型的匹配。 buildClient的泛型const T没有明确约束为契约的结构,TypeScript无法正确递归解析每个属性是否为Endpoint类型。
修正后的完整实现
下面是修复后的代码,我会标注关键的修改点:
import { z } from 'zod'; /** * Extracts path parameters (e.g., ":id") from a URL path string. */ 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 } : {}; /** * Defines the structure of a single API endpoint. * It uses a conditional type to make `pathParams` required only if params exist in the path. */ 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>> }); /** * A helper function to provide type inference for endpoints. */ function endpoint< TPath extends string, TMethod extends string, TQuery, TBody, TResponse >( def: Endpoint<TPath, TMethod, TQuery, TBody, TResponse> ): Endpoint<TPath, TMethod, TQuery, TBody, TResponse> { return def; } /** * This is the desired type for a callable endpoint function on the final client. * The arguments should be properly typed. */ type EndpointFetcher<T extends Endpoint<any, any, any, any, any>> = ( args: & (T['body'] extends z.ZodSchema<infer TBody> ? { body: TBody } : { body?: never }) & (T['query'] extends z.ZodSchema<infer TQuery> ? { query: TQuery } : { query?: never }) & ('pathParams' extends keyof T ? (T['pathParams'] extends z.ZodSchema<infer TPathParams> ? { pathParams: TPathParams } : never) : { pathParams?: never }) ) => Promise<z.infer<T['response']>>; /** * 修正后的递归客户端类型: * 1. 增加对只读类型的兼容(处理`as const`断言) * 2. 明确判断属性是否为`Endpoint`类型的只读版本 */ type ClientInfer<T> = T extends Readonly<Endpoint<infer Path, infer Method, infer Query, infer Body, infer Response>> ? EndpointFetcher<Endpoint<Path, Method, Query, Body, Response>> : T extends Readonly<Record<string, any>> ? { readonly [K in keyof T]: ClientInfer<T[K]> } : never; /** * 修正后的buildClient函数: * 1. 明确泛型约束为契约结构 * 2. 使用Readonly确保兼容`as const`断言的类型 */ declare function buildClient<const T extends Record<string, any>>( contract: T, config: { baseURL: string } ): ClientInfer<T>; // ------------------------------------------- // Example Contract & Usage // ------------------------------------------- const usersContract = { // A simple endpoint getUser: endpoint({ method: 'GET', path: '/users/:id', pathParams: z.object({ id: z.string() }), response: z.object({ id: z.string(), name: z.string() }), }), // A nested endpoint updateUser: { location: endpoint({ method: 'PUT', path: '/users/:id/location/:locationId', pathParams: z.object({ id: z.string(), locationId: z.string() }), body: z.object({ name: z.string() }), response: z.object({ success: z.boolean() }), }), }, } as const; const api = buildClient(usersContract, { baseURL: 'https://api.example.com' }); // 现在会有正确的类型检查和自动补全! const user = await api.getUser({ pathParams: { id: 'user-123' } }); // 嵌套端点也能正常工作 const result = await api.updateUser.location({ pathParams: { id: 'user-123', locationId: 'loc-456' }, body: { name: 'New Location' } });
关键修改点说明
ClientInfer类型的修正:- 增加了
Readonly包装,处理as const断言后的只读类型,确保能正确匹配Endpoint类型。 - 在判断端点类型时,显式提取
Endpoint的泛型参数,避免TypeScript在递归解析时丢失类型信息。
- 增加了
EndpointFetcher类型的优化:- 对
pathParams、query、body的类型推断做了更精确的处理,直接从Zod Schema中提取类型,而不是依赖原始的泛型参数。
- 对
buildClient函数的泛型约束:- 明确泛型
T为Record<string, any>,帮助TypeScript更好地推断嵌套结构。 - 配置参数增加了
baseURL的明确类型,让API更清晰。
- 明确泛型
验证效果
现在你的api.getUser和api.updateUser.location会获得完整的类型支持:
- 必须传入符合要求的
pathParams(如果路径有参数) - 请求体和查询参数会根据Zod Schema得到类型检查
- 返回值会自动推断为Zod Schema对应的响应类型
- 所有参数都有自动补全提示
内容来源于stack exchange
相关产品推荐
相关产品推荐

