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 overhttpClientGet
gives me the overloads forHttpClient.get()
call()
is the wrapper function that does the sameish interception as the original, but passesHttpClient.get()
with the URL already partially applied usingFunction.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