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:
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.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
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 correspondingHttpClient
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!