import {AxiosResponse} from 'axios'; type ValueOf = T[keyof T]; type RHandler = ValueOf<{[K in keyof T]: T[K] extends (data: any) => infer U ? U : never}>; function typeGuard(checker: (x: U) => boolean) { return function(x: U): x is T { return checker(x); }; } export class BadResponseError extends Error { constructor(public res: AxiosResponse, label: string) { super(`${label} status code: ${res.status}\ndata: ${ typeof res.data === 'object' ? JSON.stringify(res.data) : res.data}`); Object.setPrototypeOf(this, BadResponseError.prototype); } } export class APIPromise< TRes, KRsv extends keyof TRes, THdl extends {[K in KRsv]: (data: TRes[K]) => any}, KOn extends keyof TRes = keyof TRes, > implements PromiseLike> { private promise: Promise>; constructor( resPromise: Promise, stps: {[K in keyof TRes]: (data: any) => TRes[K]}, private handlers: THdl, ) { this.promise = resPromise.then(res => { const {status, data} = res; if (!typeGuard(x=>stps.hasOwnProperty(x))(status)) { // unexpected status throw new BadResponseError(res, 'Unexpected'); } const r = stps[status](data); if (!typeGuard(x=>this.handlers.hasOwnProperty(x))(status)) { // unhandled status throw new BadResponseError(res, 'Unhandled'); } const handler = this.handlers[status]; return handler(r); }); } static init( res: Promise, stps: {[K in keyof TRes]: (data: any) => TRes[K]}, kRsvs: KRsv[], ): APIPromise< TRes, KRsv, {[K in KRsv]: (data: TRes[K]) => TRes[K]} > { const handlers: {[K in KRsv]: (data: TRes[K]) => TRes[K]} = {} as any; for (const kRsv of kRsvs) { handlers[kRsv] = x => x; } return new APIPromise(res, stps, handlers); } on( status: KK, handler: (data: TRes[KK]) => URst, ): APIPromise< TRes, KRsv | KK, {[K in (KRsv | KK)]: (data: TRes[K]) => K extends KK ? URst : K extends keyof THdl ? ReturnType: never}, Exclude > { const self = this as any; self.handlers[status] = handler; return self; } then( onRsv?: (value: RHandler) => RRsv|PromiseLike, onRjt?: (reason: any) => RRjt|PromiseLike, ): Promise { return this.promise.then(onRsv, onRjt); } catch( onRjt: (reason: any) => RRjt|PromiseLike, ) { return this.then(undefined, onRjt); } }