动态引用嵌套接口类型的通用 TypeScript 类型

Generic TypeScript type referencing nested interface types dynamically

抱歉,标题令人困惑!我会尽量在这里说清楚。给定以下接口(由 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 函数,以便给定路径和方法知道所需参数的类型,以及 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;
};

然而,这将无法正常工作,我收到以下错误:

  1. 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>> => {
    //...
};

所以现在类型 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 的解决方案仅适用于一种有点限制的路径。我建议的那个没有错误并且适用于任意数量的路径。

我查看了 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 类型不需要任何类型参数,而我的 RequestParamsResponseType 类型只需要 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