代码之家  ›  专栏  ›  技术社区  ›  millimoose Tomasz Nurkiewicz

如何以类型安全的方式在typescript 3.x中表示部分函数应用程序?

  •  0
  • millimoose Tomasz Nurkiewicz  · 技术社区  · 6 年前

    我正在开发一个角度代码库,它对大多数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的重载解决方案非常重要,并决定调用的返回类型。

    我正试图找到一种更简单、更柔和的、类型安全的方法来包装调用,但我不知道如何捕获选项参数的类型。

    到目前为止,我拥有的是:

    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 回调应该是相应的 HTTP客户端 方法, 除了 第一个( url )参数。或者其他一些让我以类型安全的方式来完成这项工作的方法,例如,如果我这样做的话,自动完成和过载分辨率应该可以正常工作:

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

    StackBlitz链接用于上述可运行示例: https://stackblitz.com/edit/httpclient-partial

    1 回复  |  直到 6 年前
        1
  •  1
  •   jcalz    6 年前

    有角度知识的人可以把这个问题具体化,或者给出一个更有针对性的答案,但我要解决这个问题:

    告诉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('get', 'https://example.com/foo/bar',
      get => get({
        // hints should appear here
      })
    );
    

    无论如何,希望这能帮助你找到正确的方向。祝你好运!