动态引用嵌套接口类型的通用 TypeScript 类型
Generic TypeScript type referencing nested interface types dynamically
抱歉,标题令人困惑!我会尽量在这里说清楚。给定以下接口(由 openapi-typescript
作为 API 定义生成):
export interface paths {
'/v1/some-path/:id': {
get: {
parameters: {
query: {
id: number;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
post: {
parameters: {
body: {
name: string;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
};
}
上面的接口 paths
将有许多由字符串标识的路径,每个路径都有一些可用的方法,然后定义参数和响应类型。
我正在尝试编写一个通用 apiCall
函数,以便给定路径和方法知道所需参数的类型,以及 return 类型。
这是我目前拥有的:
type Path = keyof paths;
type PathMethods<P extends Path> = keyof paths[P];
type RequestParams<P extends Path, M extends PathMethods<P>> =
paths[P][M]['parameters'];
type ResponseType<P extends Path, M extends PathMethods<P>> =
paths[P][M]['responses'][200]['schema'];
export const apiCall = (
path: Path,
method: PathMethods<typeof path>,
params: RequestParams<typeof path, typeof method>
): Promise<ResponseType<typeof path, typeof method>> => {
const url = path;
console.log('params', params);
// method & url are
return fetch(url, { method }) as any;
};
然而,这将无法正常工作,我收到以下错误:
paths[P][M]['parameters']['path']
-> Type '"parameters"' cannot be used to index type 'paths[P][M]'
即使它确实有效(如果我这样做 type test = RequestParams<'/v1/some-path/:id', 'get'>
然后 test
显示正确的类型)
知道如何实现吗?
解决方案
经过几次尝试,这是我找到的解决方案。
首先,我使用条件类型来定义RequestParams
:
type RequestParams<P extends Path, M extends PathMethods<P>> =
"parameters" extends keyof paths[P][M]
? paths[P][M]["parameters"]
: undefined;
因为 typescript 动态推断出 path
的类型,所以键 parameters
可能不存在,所以我们不能使用它。 条件类型 检查这种特定情况。
可以对 ResponseType
执行同样的操作(这会更冗长)以访问 typescript 抱怨的属性。
然后,我更新了函数的签名apiCall
:
export const apiCall = <P extends Path, M extends PathMethods<P>>(
path: P,
method: M,
params: RequestParams<P, M>
): Promise<ResponseType<P, M>> => {
//...
};
所以现在类型 P
和 M
绑定在一起。
奖金
最后,如果不需要参数,我再次使用条件类型将参数 params
设为可选:
export const apiCall = <P extends Path, M extends PathMethods<P>>(
path: P,
method: M,
...params: RequestParams<P, M> extends undefined ? []: [RequestParams<P, M>]
): Promise<ResponseType<P, M>> => {
//...
};
这是一个有效的 typescript playground 解决方案。我添加了一个没有参数的方法 delete
只是为了测试最终用例。
来源
编辑
这里是更新后的 typescript playground 错误。
此外,我看到 Alessio 的解决方案仅适用于一种有点限制的路径。我建议的那个没有错误并且适用于任意数量的路径。
我查看了 Baboo's solution by following the link 他的 TypeScript playground。
在第 57 行,ResponseType
类型给出以下错误:
Type '"responses"' cannot be used to index type 'paths[P][M]'.(2536)
Type '200' cannot be used to index type 'paths[P][M]["responses"]'.(2536)
Type '"schema"' cannot be used to index type 'paths[P][M]["responses"][200]'.(2536)
我从那个解决方案开始做了一些工作,并且没有错误地获得了所需的功能,并且使用了稍微简单的类型定义,这需要更少的类型参数。
特别是,我的 PathMethod
类型不需要任何类型参数,而我的 RequestParams
和 ResponseType
类型只需要 1 个类型参数。
这里是 TypeScript playground 的完整解决方案。
根据 captain-yossarian 评论中的要求,这里是完整的解决方案:
export interface paths {
'/v1/some-path/:id': {
get: {
parameters: {
query: {
id: number;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
post: {
parameters: {
body: {
name: string;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
delete: {
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
};
}
type Path = keyof paths;
type PathMethod = keyof paths[Path];
type RequestParams<T extends PathMethod> = paths[Path][T] extends {parameters: any} ? paths[Path][T]['parameters'] : undefined;
type ResponseType<T extends PathMethod> = paths[Path][T] extends {responses: {200: {schema: {[x: string]: any}}}} ? keyof paths[Path][T]['responses'][200]['schema'] : undefined;
export const apiCall = <P extends Path, M extends PathMethod>(
path: P,
method: M,
...params: RequestParams<M> extends undefined ? [] : [RequestParams<M>]
): Promise<ResponseType<M>> => {
const url = path;
console.log('params', params);
return fetch(url, { method }) as any;
};
更新:
在评论中,aurbano 指出我的解决方案仅在 paths
只有 1 个密钥时才有效。这是一个更新的解决方案,适用于 2 个不同的路径。
export interface paths {
'/v1/some-path/:id': {
get: {
parameters: {
query: {
id: number;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
post: {
parameters: {
body: {
name: string;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
delete: {
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
};
'/v2/some-path/:id': {
patch: {
parameters: {
path: {
id: number;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
};
}
type Path = keyof paths;
type PathMethod<T extends Path> = keyof paths[T];
type RequestParams<P extends Path, M extends PathMethod<P>> = paths[P][M] extends {parameters: any} ? paths[P][M]['parameters'] : undefined;
type ResponseType<P extends Path, M extends PathMethod<P>> = paths[P][M] extends {responses: {200: {schema: {[x: string]: any}}}} ? keyof paths[P][M]['responses'][200]['schema'] : undefined;
export const apiCall = <P extends Path, M extends PathMethod<P>>(
path: P,
method: M,
...params: RequestParams<P, M> extends undefined ? [] : [RequestParams<P, M>]
): Promise<ResponseType<P, M>> => {
const url = path;
console.log('params', params);
return fetch(url, { method: method as string }) as any;
};
apiCall("/v1/some-path/:id", "get", {
header: {},
query: {
id: 1
}
}); // Passes -> OK
apiCall("/v2/some-path/:id", "get", {
header: {},
query: {
id: 1
}
}); // Type error -> OK
apiCall("/v2/some-path/:id", "patch", {
header: {},
query: {
id: 1
}
}); // Type error -> OK
apiCall("/v2/some-path/:id", "patch", {
header: {},
path: {
id: 1,
}
}); // Passes -> OK
apiCall("/v1/some-path/:id", "get", {
header: {},
query: {
id: 'ee'
}
}); // Type error -> OK
apiCall("/v1/some-path/:id", "get", {
query: {
id: 1
}
}); // Type error -> OK
apiCall("/v1/some-path/:id", "get"); // Type error -> OK
apiCall("/v1/some-path/:id", 'delete'); // Passes -> OK
apiCall("/v1/some-path/:id", "delete", {
header: {},
query: {
id: 1
}
}); // Type error -> OK
这是更新后的 playground。
抱歉,标题令人困惑!我会尽量在这里说清楚。给定以下接口(由 openapi-typescript
作为 API 定义生成):
export interface paths {
'/v1/some-path/:id': {
get: {
parameters: {
query: {
id: number;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
post: {
parameters: {
body: {
name: string;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
};
}
上面的接口 paths
将有许多由字符串标识的路径,每个路径都有一些可用的方法,然后定义参数和响应类型。
我正在尝试编写一个通用 apiCall
函数,以便给定路径和方法知道所需参数的类型,以及 return 类型。
这是我目前拥有的:
type Path = keyof paths;
type PathMethods<P extends Path> = keyof paths[P];
type RequestParams<P extends Path, M extends PathMethods<P>> =
paths[P][M]['parameters'];
type ResponseType<P extends Path, M extends PathMethods<P>> =
paths[P][M]['responses'][200]['schema'];
export const apiCall = (
path: Path,
method: PathMethods<typeof path>,
params: RequestParams<typeof path, typeof method>
): Promise<ResponseType<typeof path, typeof method>> => {
const url = path;
console.log('params', params);
// method & url are
return fetch(url, { method }) as any;
};
然而,这将无法正常工作,我收到以下错误:
paths[P][M]['parameters']['path']
->Type '"parameters"' cannot be used to index type 'paths[P][M]'
即使它确实有效(如果我这样做type test = RequestParams<'/v1/some-path/:id', 'get'>
然后test
显示正确的类型)
知道如何实现吗?
解决方案
经过几次尝试,这是我找到的解决方案。
首先,我使用条件类型来定义RequestParams
:
type RequestParams<P extends Path, M extends PathMethods<P>> =
"parameters" extends keyof paths[P][M]
? paths[P][M]["parameters"]
: undefined;
因为 typescript 动态推断出 path
的类型,所以键 parameters
可能不存在,所以我们不能使用它。 条件类型 检查这种特定情况。
可以对 ResponseType
执行同样的操作(这会更冗长)以访问 typescript 抱怨的属性。
然后,我更新了函数的签名apiCall
:
export const apiCall = <P extends Path, M extends PathMethods<P>>(
path: P,
method: M,
params: RequestParams<P, M>
): Promise<ResponseType<P, M>> => {
//...
};
所以现在类型 P
和 M
绑定在一起。
奖金
最后,如果不需要参数,我再次使用条件类型将参数 params
设为可选:
export const apiCall = <P extends Path, M extends PathMethods<P>>(
path: P,
method: M,
...params: RequestParams<P, M> extends undefined ? []: [RequestParams<P, M>]
): Promise<ResponseType<P, M>> => {
//...
};
这是一个有效的 typescript playground 解决方案。我添加了一个没有参数的方法 delete
只是为了测试最终用例。
来源
编辑
这里是更新后的 typescript playground 错误。
此外,我看到 Alessio 的解决方案仅适用于一种有点限制的路径。我建议的那个没有错误并且适用于任意数量的路径。
我查看了 Baboo's solution by following the link 他的 TypeScript playground。
在第 57 行,ResponseType
类型给出以下错误:
Type '"responses"' cannot be used to index type 'paths[P][M]'.(2536)
Type '200' cannot be used to index type 'paths[P][M]["responses"]'.(2536)
Type '"schema"' cannot be used to index type 'paths[P][M]["responses"][200]'.(2536)
我从那个解决方案开始做了一些工作,并且没有错误地获得了所需的功能,并且使用了稍微简单的类型定义,这需要更少的类型参数。
特别是,我的 PathMethod
类型不需要任何类型参数,而我的 RequestParams
和 ResponseType
类型只需要 1 个类型参数。
这里是 TypeScript playground 的完整解决方案。
根据 captain-yossarian 评论中的要求,这里是完整的解决方案:
export interface paths {
'/v1/some-path/:id': {
get: {
parameters: {
query: {
id: number;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
post: {
parameters: {
body: {
name: string;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
delete: {
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
};
}
type Path = keyof paths;
type PathMethod = keyof paths[Path];
type RequestParams<T extends PathMethod> = paths[Path][T] extends {parameters: any} ? paths[Path][T]['parameters'] : undefined;
type ResponseType<T extends PathMethod> = paths[Path][T] extends {responses: {200: {schema: {[x: string]: any}}}} ? keyof paths[Path][T]['responses'][200]['schema'] : undefined;
export const apiCall = <P extends Path, M extends PathMethod>(
path: P,
method: M,
...params: RequestParams<M> extends undefined ? [] : [RequestParams<M>]
): Promise<ResponseType<M>> => {
const url = path;
console.log('params', params);
return fetch(url, { method }) as any;
};
更新:
在评论中,aurbano 指出我的解决方案仅在 paths
只有 1 个密钥时才有效。这是一个更新的解决方案,适用于 2 个不同的路径。
export interface paths {
'/v1/some-path/:id': {
get: {
parameters: {
query: {
id: number;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
post: {
parameters: {
body: {
name: string;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
delete: {
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
};
'/v2/some-path/:id': {
patch: {
parameters: {
path: {
id: number;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
};
}
type Path = keyof paths;
type PathMethod<T extends Path> = keyof paths[T];
type RequestParams<P extends Path, M extends PathMethod<P>> = paths[P][M] extends {parameters: any} ? paths[P][M]['parameters'] : undefined;
type ResponseType<P extends Path, M extends PathMethod<P>> = paths[P][M] extends {responses: {200: {schema: {[x: string]: any}}}} ? keyof paths[P][M]['responses'][200]['schema'] : undefined;
export const apiCall = <P extends Path, M extends PathMethod<P>>(
path: P,
method: M,
...params: RequestParams<P, M> extends undefined ? [] : [RequestParams<P, M>]
): Promise<ResponseType<P, M>> => {
const url = path;
console.log('params', params);
return fetch(url, { method: method as string }) as any;
};
apiCall("/v1/some-path/:id", "get", {
header: {},
query: {
id: 1
}
}); // Passes -> OK
apiCall("/v2/some-path/:id", "get", {
header: {},
query: {
id: 1
}
}); // Type error -> OK
apiCall("/v2/some-path/:id", "patch", {
header: {},
query: {
id: 1
}
}); // Type error -> OK
apiCall("/v2/some-path/:id", "patch", {
header: {},
path: {
id: 1,
}
}); // Passes -> OK
apiCall("/v1/some-path/:id", "get", {
header: {},
query: {
id: 'ee'
}
}); // Type error -> OK
apiCall("/v1/some-path/:id", "get", {
query: {
id: 1
}
}); // Type error -> OK
apiCall("/v1/some-path/:id", "get"); // Type error -> OK
apiCall("/v1/some-path/:id", 'delete'); // Passes -> OK
apiCall("/v1/some-path/:id", "delete", {
header: {},
query: {
id: 1
}
}); // Type error -> OK
这是更新后的 playground。