0

I'm working on an Angular codebase that does some standard postprocessing on most API calls. This is done in a service class that wraps HttpClient.get() etc. in methods that pipe the returned observable through a bunch of intercepting methods.

To my dismay this is done using the pattern:

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

which annoys me, because the options parameter has a fairly hairy type the exact shape of which is important for TypeScript's overload resolution and determines the return type of the call.

I'm trying to figure out a less copy-pastey, typesafe way of wrapping the call, but I can't figure out how to capture the type of the options parameter.

What I have so far is:

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

That is:

  • I know I can capture the signature of the method I'm calling on HttpClient by passing its name as a string literal to a method correctly, hovering over httpClientGet gives me the overloads for HttpClient.get()
  • call() is the wrapper function that does the sameish interception as the original, but passes HttpClient.get() with the URL already partially applied using Function.bind() to an optional callback.
  • The role of this callback is to provide the value of the options parameter to from the HttpClient methods if the caller wants to.

Where I'm lost is figuring out what the right construct is to tell TypeScript that the parameters of the partial callback should be the parameters of the corresponding HttpClient method, except the first (url) parameter. Or some alternative way letting me do this in a type-safe fashion, i.e. autocomplete and overload resolution should work correctly if I do:

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

Stackblitz link for a runnable example of the above: https://stackblitz.com/edit/httpclient-partial

millimoose
  • 39,073
  • 9
  • 82
  • 134

1 Answers1

2

Someone with a knowledge of angular can flesh this out or give a more targeted answer, but I'm going to address this question:

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

If you are trying to strip the first parameter off a function type, this is possible in TypeScript 3.0 and up:

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

So for your call() method I'd imagine it looking something like this:

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;

where I've intentionally left out the return type of call and the supertype of TO that you apparently already know how to deal with.

The important part is that args rest parameter, which is inferred to be the same as the arguments of HttpClient[M] with the first parameter stripped off. That should give you the hints you expect when you call call():

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

Anyway, hope that helps point you in the right direction. Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • I've since stumbled on this and it moved me onwards alas in the direction of an even deeper swamp that I'll have to turn into its own question or a few. (The return type actually does not quite work, because HttpClient returns something different in an overload based on the value of `options`, and I'm starting to have doubts what I want can actually work without full on macros.) That said this addresses my question as stated and if nobody else comes up with something much better I'll acccept it. – millimoose Dec 05 '18 at 03:16