如何以类型安全的方式在 Typescript 3.x 中表达偏函数应用?

How do I express partial function application in Typescript 3.x in a type-safe way?

我正在开发一个 Angular 代码库,它对大多数 API 调用进行一些标准的后处理。这是在服务 class 中完成的,该服务将 HttpClient.get() 等包装在通过一堆拦截方法管道 returned observable 的方法中。

令我沮丧的是,这是使用以下模式完成的:

public get(url, options?) {
  const method = 'GET';
  return this._http.get(url, options).pipe(
    map((resp: any) => {
      console.log(`Calling ${method} ${url} returned`, resp);
      return resp;
    }),
    catchError(err => {
      console.error(`Calling ${method} ${url} failed`, err);
      throw(err);
    }),
  );
}

这让我很烦,因为选项参数有一个相当多毛的类型,它的确切形状对于 TypeScript 的重载解析很重要,并决定了调用的 return 类型。

我正在尝试找出一种更少复制粘贴、类型安全的包装调用的方法,但我不知道如何捕获选项参数的类型。

我目前拥有的是:

export class HelloComponent {
  @Input() name: string;
  response: any;

  constructor(private _http: HttpClient) {
    const httpClientGet = this.method('get');

    const response = this.call('get', 'https://example.com/foo/bar');

    response.subscribe(
      data => this.response = JSON.stringify(data, null, 2),
      (err: HttpErrorResponse) => this.response = err.error
    );

  }

  call<T>(
    method: keyof HttpClient,
    url: string,
    handler: <TObs extends Observable<T>>(partial: (options?: any) => TObs) => TObs = (_ => _())) /* HOW DO I GET THE CORRECT TYPE OF OPTIONS HERE? */
    : Observable<T> {

    const u = new URL(url);
    console.info(`Calling ${method.toUpperCase()} ${u.pathname}`);

    const result = handler(this._http[method].bind(this._http, url)).pipe(
      map((resp) => {
        console.log(`Calling ${method.toUpperCase()} ${u.pathname} returned`, resp);
        return resp;
      }),
      catchError(err => {
        console.error(`Calling ${method.toUpperCase()} ${u.pathname} failed`, err);
        throw err;
      })
    )

    console.info('Returning', result);
    return result;
  }

  method<TMethod extends keyof HttpClient>(name: TMethod): HttpClient[TMethod] {
    return this._http[name];
  }
}

即:

我迷路的地方是弄清楚什么是正确的构造来告诉 TypeScript partial 回调的参数应该是相应 HttpClient 方法的参数,except 第一个 (url) 参数。或者让我以类型安全的方式执行此操作的其他方法,即如果我这样做,自动完成和重载解析应该可以正常工作:

this.call('get', 'https://example.com/foo/bar',
  get => get({
    // options for `HttpClient.get()`
  })
);

Stackblitz link 上面的可运行示例:https://stackblitz.com/edit/httpclient-partial

了解 angular 的人可以充实这个或给出更有针对性的答案,但我要解决这个问题:

tell TypeScript that the parameters of the partial callback should be the parameters of the corresponding HttpClient method, except the first (url) parameter.

如果您试图从函数类型中去除第一个参数,这在 TypeScript 3.0 及更高版本中是可行的:

type StripFirstParam<F> = 
  F extends (first: any, ...rest: infer A)=>infer R ? (...args: A)=>R : never

所以对于你的 call() 方法,我想象它看起来 某些东西 像这样:

declare function call<M extends keyof HttpClient>(
  method: M, 
  url: string, 
  handler: <TO>(
    partial: (
      ...args: (HttpClient[M] extends (x: any, ...rest: infer A) => any ? A : never)
    ) => TO
  ) => TO
): void;

我故意遗漏了 call 的 return 类型和 TO 的超类型,您显然已经知道如何处理。

重要的部分是args剩余参数,它被推断与HttpClient[M]的参数相同,但第一个参数被剥离。这应该会给你你打电话时所期望的提示 call():

call('get', 'https://example.com/foo/bar',
  get => get({
    // hints should appear here
  })
);

无论如何,希望能帮助您指明正确的方向。祝你好运!