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

Я работаю над кодовой базой 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 для методов 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 Ответ

0 голосов
/ 05 декабря 2018

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

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

Если вы пытаетесь удалить первый параметр из типа функции, это возможно в TypeScript 3.0 и выше:

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

Так что для вашего call() метода I 'я мог бы представить, что это выглядит что-то примерно так:

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

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

...