Как я могу выразить частичное применение функции в Typescript 3.x безопасным для типов способом?

Я работаю над кодовой базой Angular, которая выполняет некоторую стандартную постобработку для большинства вызовов API. Это делается в классе обслуживания, который переносит HttpClient.get() и т.д. в методах, которые передают возвращаемое наблюдаемое через группу перехватывающих методов.

К моему ужасу это делается с помощью шаблона:

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

это меня раздражает, потому что параметр options имеет довольно волосатый тип, точная форма которого важна для разрешения перегрузки TypeScript и определяет тип возврата вызова.

Я пытаюсь найти менее защищенный от копирования, типизированный способ упаковки вызова, но я не могу понять, как захватить тип параметра options.

То, что я до сих пор это:

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

То есть:

  • Я знаю, что могу захватить подпись метода, который я вызываю HttpClient передавая его имя в виде строкового литерала методу правильно, нависая над httpClientGet дает мне перегрузки для HttpClient.get()
  • call() является функцией-оберткой, которая выполняет тот же перехват, что и оригинал, но передает HttpClient.get() с уже частично примененным URL с помощью Function.bind() на дополнительный обратный вызов.
  • Роль этого обратного вызова заключается в предоставлении значения options параметр to из методов HttpClient, если вызывающая сторона хочет.

Я теряюсь в том, чтобы выяснить, какова правильная конструкция, чтобы сказать TypeScript, что параметры partial обратным вызовом должны быть параметры соответствующего HttpClient метод, кроме первого (url) параметр. Или какой-то альтернативный способ, позволяющий мне сделать это безопасным для типов способом, то есть автозаполнение и разрешение перегрузки должны работать правильно, если я это сделаю:

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

Ссылка Stackblitz для работающего примера выше: https://stackblitz.com/edit/httpclient-partial

1 ответ

Решение

Кто-то, обладающий знанием углов, может конкретизировать это или дать более целенаправленный ответ, но я собираюсь ответить на этот вопрос:

скажите TypeScript, что параметры partial обратным вызовом должны быть параметры соответствующего HttpClient метод, кроме первого (url) параметр.

Если вы пытаетесь удалить первый параметр из типа функции, это возможно в 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 и супертип TO что вы, видимо, уже знаете, как бороться.

Важной частью является то, что args параметр rest, который выводится так же, как аргументы HttpClient[M] с первым удаленным параметром. Это должно дать вам подсказки, которые вы ожидаете, когда вы звоните call():

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

В любом случае, надеюсь, это поможет вам в правильном направлении. Удачи!

Другие вопросы по тегам