【问题标题】:Generic TypeScript type referencing nested interface types dynamically动态引用嵌套接口类型的通用 TypeScript 类型
【发布时间】:2021-09-20 22:21:30
【问题描述】:

对不起,令人困惑的标题!我会尽量在这里尽可能清楚。给定以下接口(由openapi-typescript 作为 API 定义生成):

TypeScript playground 看看这个在行动中

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 函数,这样给定一个路径和一个方法就知道所需参数的类型和返回类型。

这是我目前所拥有的:

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;
};

但是这不能正常工作,我收到以下错误:

  1. paths[P][M]['parameters']['path'] -> Type '"parameters"' cannot be used to index type 'paths[P][M]' 即使它确实有效(如果我这样做 type test = RequestParams&lt;'/v1/some-path/:id', 'get'&gt; 然后 test 显示正确的类型)

知道如何实现吗?

【问题讨论】:

  • 你能确定这里的代码是minimal reproducible example吗?我有a few issues;一个是'path' 确实似乎在这里应该是一个错误,我无法重现您的“method 由于某种原因变成never 类型”。大概你希望apiCall 成为一个通用函数,但我看不出你的问题是什么。此外,您可能希望将其拆分为多个问题;您的错误 1 ​​和 2 似乎彼此相关,但 3 只是您在代码中遇到的不同问题。
  • 我可以减少一点,但它可能会消除主要问题 -> 主要是路径和方法是确定响应类型所必需的,而从某种意义上说,它们都是“通用的”它们是由路径决定的。问题 1 和问题 2 相同,问题 3 可能不是问题 -> 参见我添加的 TypeScript 游乐场

标签: ajax typescript openapi type-inference typescript-generics


【解决方案1】:

解决方案

经过几次尝试,这是我找到的解决方案。

首先,我使用条件类型来定义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>> => {
    //...
};

所以现在PM 类型绑定在一起了。

奖金

最后,如果不需要参数,我再次使用条件类型将参数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 的解决方案只适用于一个有点限制的路径。我建议的那个没有错误,适用于任意数量的路径。

【讨论】:

  • 感谢您的贡献!但是请参阅@alessio 的答案-不幸的是,这仍然存在一些 TS 问题:(
  • @aurbano 这里是更新的解决方案:没有错误并且支持任意数量的路径
  • 太棒了!非常感谢:D
  • 以防万一将来有人浏览此内容,如果我们希望 ResponseType 成为实际的响应对象,则不应使用 keyof
【解决方案2】:

我按照link 到他的 TypeScript 游乐场检查了Baboo 的解决方案。 在第 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 类型不需要任何类型参数,而我的RequestParamsResponseType 类型只需要1 个类型参数。

这里是TypeScript playground 的完整解决方案。

根据captain-yossarian 在 cmets 中的要求,这是完整的解决方案:

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;
};

更新:

在 cmets 中,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

【讨论】:

  • 请在您的答案中分享可重现的示例。 TS 游乐场链接不可靠
  • 这看起来很有希望!!然而,它似乎只在paths 界面中有一条路径时才有效 - 我只留下一条来简化示例,但请查看问题中更新的游乐场:(
  • 为了精确起见,我在@baboo 更新他的解决方案并写道我的解决方案受到限制前 1 小时更新了我的解决方案,使其没有错误和多条路径。
猜你喜欢
  • 1970-01-01
  • 2020-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-09-13
相关资源
最近更新 更多