G
G
GF2021-01-12 18:14:31
Angular
GF, 2021-01-12 18:14:31

How to create a wrapper around HttpClient?

I'm trying to create a wrapper over HttpClient, but I can't describe the types normally.
HttpClient has 15 overloads per method ( https://angular.io/api/common/http/HttpClient )
Since they are very similar, I decided to make the same content into a common type and did the following:

type Observe = 'body' | 'events' | 'response';
type ResponseType = 'arraybuffer' | 'blob' | 'text' | 'json';

interface Params<O extends Observe, R extends ResponseType> {
  url: string;
  body: any | null;
  options?: {
    headers?:
      | HttpHeaders
      | {
          [header: string]: string | string[];
        };
    observe?: O;
    params?:
      | HttpParams
      | {
          [param: string]: string | string[];
        };
    reportProgress?: boolean;
    responseType: R;
    withCredentials?: boolean;
  };
}
export class MyHttpClient {
  constructor(private readonly http: HttpClient) {}

  put(params: Params<'body', 'arraybuffer'>): Observable<ArrayBuffer>; // This overload signature is not compatible with its implementation signature.
  put(params: Params<'body', 'blob'>): Observable<Blob>;
  put(params: Params<'body', 'text'>): Observable<string>;
  put(params: Params<'events', 'arraybuffer'>): Observable<HttpEvent<ArrayBuffer>>;
  put(params: Params<'events', 'blob'>): Observable<HttpEvent<Blob>>;
  put(params: Params<'events', 'text'>): Observable<HttpEvent<string>>;
  put(params: Params<'events', 'json'>): Observable<HttpEvent<Object>>;
  put<T>(params: Params<'events', 'json'>): Observable<HttpEvent<T>>;
  put(params: Params<'response', 'arraybuffer'>): Observable<HttpResponse<ArrayBuffer>>;
  put(params: Params<'response', 'blob'>): Observable<HttpResponse<Blob>>;
  put(params: Params<'response', 'text'>): Observable<HttpResponse<string>>;
  put(params: Params<'response', 'json'>): Observable<HttpResponse<Object>>;
  put<T>(params: Params<'response', 'json'>): Observable<HttpResponse<T>>;
  put(params: Params<'body', 'json'>): Observable<Object>;
  put<T>(params: Params<'body', 'json'>): Observable<T> {
    return this.http.put<T>(params.url, params.body, params.options);
  }
}

All overloads are in the same sequence as in the original client
. But the typescript considers the types to be incomparable (This overload signature is not compatible with its implementation signature.)

Does anyone have experience creating such or a similar wrapper? Is it possible to do this while keeping all types? Maybe some more advice as to what I'm trying to do... Help

Answer the question

In order to leave comments, you need to log in

1 answer(s)
D
Dmitry Belyaev, 2021-01-12
@fomenkogregory

An implementation must have the most common signature from all overloads. Moreover, the types in the implementation work only inside, and only overloads stick out:

export class MyHttpClient {
  constructor(private readonly http: HttpClient) {}

  put(params: Params<'body', 'arraybuffer'>): Observable<ArrayBuffer>;
  put(params: Params<'body', 'blob'>): Observable<Blob>;
  put(params: Params<'body', 'text'>): Observable<string>;
  put(params: Params<'events', 'arraybuffer'>): Observable<HttpEvent<ArrayBuffer>>;
  put(params: Params<'events', 'blob'>): Observable<HttpEvent<Blob>>;
  put(params: Params<'events', 'text'>): Observable<HttpEvent<string>>;
  put(params: Params<'events', 'json'>): Observable<HttpEvent<Object>>;
  put<T>(params: Params<'events', 'json'>): Observable<HttpEvent<T>>;
  put(params: Params<'response', 'arraybuffer'>): Observable<HttpResponse<ArrayBuffer>>;
  put(params: Params<'response', 'blob'>): Observable<HttpResponse<Blob>>;
  put(params: Params<'response', 'text'>): Observable<HttpResponse<string>>;
  put(params: Params<'response', 'json'>): Observable<HttpResponse<Object>>;
  put<T>(params: Params<'response', 'json'>): Observable<HttpResponse<T>>;
  put(params: Params<'body', 'json'>): Observable<Object>;
  put(params: Params<Observe, ResponseType>): Observable<unknown> {
    return this.http.put(params.url, params.body, params.options as Params<'body', 'json'>['options']);
  }
}

In general, for some reason it seems to me that it will be much easier this way:
type ResponseTypeMap = {
  arraybuffer: ArrayBuffer;
  blob: Blob;
  text: string;
  json: Object;
};
type ResponseTypes = keyof ResponseTypeMap;
type ObserveWrapperMap<T extends ResponseTypeMap[ResponseTypes]> = {
  body: Observable<T>;
  events: Observable<HttpEvent<T>>;
  response: Observable<HttpResponse<T>>;
};
type ObserveWrappers = keyof ObserveWrapperMap<ResponseTypeMap[ResponseTypes]>;
type Result<O extends ObserveWrappers, R extends ResponseTypes> = ObserveWrapperMap<ResponseTypeMap[R]>[O];
type Params<O extends ObserveWrappers, R extends ResponseTypes> = {
  url: string;
  body: any;
  options?: {
    headers?: HttpHeaders | Record<string, string | string[]>;
    observe?: O;
    params?: HttpParams | Record<string, string | string[]>;
    reportProgress?: boolean;
    responseType: R;
    withCredentials?: boolean;
  };
}

export class MyHttpClient {
  constructor(private readonly http: HttpClient) {}

  put<O extends ObserveWrappers = 'body', R extends ResponseTypes = 'json'>(params: Params<O, R>): Result<O, R> {
    const {url, body, options} = params as Params<'body', 'json'>;
    return this.http.put(url, body, options) as Result<O, R>;
  }
}
especially if there are other methods besides put

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question