Skip to content

Commit

Permalink
feat: Improve typings when using createApiFetcher() - use TS generics
Browse files Browse the repository at this point in the history
  • Loading branch information
MattCCC committed Jul 4, 2024
1 parent d42267c commit ab957fe
Show file tree
Hide file tree
Showing 10 changed files with 90 additions and 83 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const response = await api.getUserDetails({ userId: 1, ratings: [1, 2] });
// GET to: http://example.com/api/posts/myTestSubject?additionalInfo=something
const response = await api.getPosts(
{ additionalInfo: 'something' },
{ subject: 'myTestSubject' }
{ subject: 'myTestSubject' },
);

// Send POST request to update userId "1"
Expand Down Expand Up @@ -258,18 +258,18 @@ interface EndpointsList extends Endpoints {
fetchTVSeries: Endpoint;
}

const api = createApiFetcher({
const api = createApiFetcher<EndpointsList>({
axios,
// Your config
}) as unknown as EndpointsList;
});

// Will return an error since "newMovies" should be a boolean
const movies = api.fetchMovies({ newMovies: 1 });

// You can also pass type to the request directly
const movie = api.fetchMovies<MoviesResponseData>(
{ newMovies: 1 },
{ movieId: 1 }
{ movieId: 1 },
);
```

Expand Down Expand Up @@ -317,7 +317,7 @@ async function sendMessage() {
}
console.log(error.config);
},
}
},
);

console.log('Message sent successfully');
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"build:node": "tsup src/index.ts --dts --format cjs --sourcemap --env.NODE_ENV production --minify",
"types-check": "tsc --noEmit",
"test": "jest --forceExit",
"lint": "eslint ./src/**/*.ts",
"lint": "eslint ./src/**/*.ts ./test/**/*.spec.ts",
"release": "npm version patch && git push --tags",
"prepare": "npm run build",
"size": "size-limit",
Expand Down
19 changes: 11 additions & 8 deletions src/api-handler.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

Check failure on line 1 in src/api-handler.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and windows-latest

Delete `␍`
// 3rd party libs

Check failure on line 2 in src/api-handler.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and windows-latest

Delete `␍`
import { applyMagic, MagicalClass } from 'js-magic';

Check failure on line 3 in src/api-handler.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and windows-latest

Delete `␍`

Check failure on line 4 in src/api-handler.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and windows-latest

Delete `␍`
// Types

Check failure on line 5 in src/api-handler.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and windows-latest

Delete `␍`
import type { AxiosInstance } from 'axios';

Check failure on line 6 in src/api-handler.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and windows-latest

Delete `␍`

Check failure on line 7 in src/api-handler.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and windows-latest

Delete `␍`
import {

Check failure on line 8 in src/api-handler.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and windows-latest

Delete `␍`
IRequestResponse,
RequestResponse,

Check failure on line 9 in src/api-handler.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and windows-latest

Delete `␍`
APIHandlerConfig,

Check failure on line 10 in src/api-handler.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and windows-latest

Delete `␍`
EndpointConfig,
EndpointsConfig,
} from './types/http-request';

import { RequestHandler } from './request-handler';
Expand All @@ -30,7 +31,7 @@ export class ApiHandler implements MagicalClass {
/**
* Endpoints
*/
protected endpoints: Record<string, EndpointConfig>;
protected endpoints: EndpointsConfig<string>;

/**
* Logger
Expand Down Expand Up @@ -89,6 +90,7 @@ export class ApiHandler implements MagicalClass {
/**
* Maps all API requests
*
* @private
* @param {*} prop Caller
* @returns {Function} Tailored request function
*/
Expand All @@ -111,7 +113,7 @@ export class ApiHandler implements MagicalClass {
* @param {*} args Arguments
* @returns {Promise} Resolvable API provider promise
*/
public async handleRequest(...args: any): Promise<IRequestResponse> {
public async handleRequest(...args: string[]): Promise<RequestResponse> {
const prop = args[0];
const endpointSettings = this.endpoints[prop];

Expand All @@ -120,7 +122,7 @@ export class ApiHandler implements MagicalClass {
const requestConfig = args[3] || {};

const uri = endpointSettings.url.replace(/:[a-z]+/gi, (str: string) =>
uriParams[str.substring(1)] ? uriParams[str.substring(1)] : str
uriParams[str.substring(1)] ? uriParams[str.substring(1)] : str,
);

let responseData = null;
Expand All @@ -146,7 +148,7 @@ export class ApiHandler implements MagicalClass {
* @param prop Method Name
* @returns {Promise}
*/
protected handleNonImplemented(prop: string): Promise<any> {
protected handleNonImplemented(prop: string): Promise<null> {
if (this.logger?.log) {
this.logger.log(`${prop} endpoint not implemented.`);
}
Expand All @@ -155,5 +157,6 @@ export class ApiHandler implements MagicalClass {
}
}

export const createApiFetcher = (options: APIHandlerConfig) =>
new ApiHandler(options);
export const createApiFetcher = <AllEndpointsList = { [x: string]: unknown }>(
options: APIHandlerConfig,
) => new ApiHandler(options) as ApiHandler & AllEndpointsList;
27 changes: 14 additions & 13 deletions src/request-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { RequestErrorHandler } from './request-error-handler';

// Types
import type {
IRequestData,
IRequestResponse,
RequestData,
RequestResponse,
ErrorHandlingStrategy,
RequestHandlerConfig,
EndpointConfig,
Expand Down Expand Up @@ -125,6 +125,7 @@ export class RequestHandler implements MagicalClass {
/**
* Maps all API requests
*
* @private
* @param {string} url Url
* @param {*} data Payload
* @param {EndpointConfig} config Config
Expand Down Expand Up @@ -152,8 +153,8 @@ export class RequestHandler implements MagicalClass {
type: Method,
url: string,
data: any = null,
config: EndpointConfig = null
): Promise<IRequestResponse> {
config: EndpointConfig = null,
): Promise<RequestResponse> {
return this.handleRequest({
type,
url,
Expand All @@ -175,7 +176,7 @@ export class RequestHandler implements MagicalClass {
method: string,
url: string,
data: any,
config: EndpointConfig
config: EndpointConfig,
): EndpointConfig {
const methodLowerCase = method.toLowerCase() as Method;
const key =
Expand All @@ -200,7 +201,7 @@ export class RequestHandler implements MagicalClass {
*/
protected processRequestError(
error: RequestError,
requestConfig: EndpointConfig
requestConfig: EndpointConfig,
): void {
if (this.isRequestCancelled(error, requestConfig)) {
return;
Expand All @@ -213,7 +214,7 @@ export class RequestHandler implements MagicalClass {

const errorHandler = new RequestErrorHandler(
this.logger,
this.requestErrorService
this.requestErrorService,
);

errorHandler.process(error);
Expand All @@ -228,8 +229,8 @@ export class RequestHandler implements MagicalClass {
*/
protected async outputErrorResponse(
error: RequestError,
requestConfig: EndpointConfig
): Promise<IRequestResponse> {
requestConfig: EndpointConfig,
): Promise<RequestResponse> {
const isRequestCancelled = this.isRequestCancelled(error, requestConfig);
const errorHandlingStrategy = requestConfig.strategy || this.strategy;

Expand Down Expand Up @@ -265,7 +266,7 @@ export class RequestHandler implements MagicalClass {
*/
public isRequestCancelled(
error: RequestError,
_requestConfig: EndpointConfig
_requestConfig: EndpointConfig,
): boolean {
return this.axios.isCancel(error);
}
Expand Down Expand Up @@ -302,7 +303,7 @@ export class RequestHandler implements MagicalClass {
// Generate unique key as a cancellation token. Make sure it fits Map
const key = JSON.stringify([method, baseURL, url, params, data]).substring(
0,
55 ** 5
55 ** 5,
);
const previousRequest = this.requestsQueue.get(key);

Expand Down Expand Up @@ -335,14 +336,14 @@ export class RequestHandler implements MagicalClass {
url,
data = null,
config = null,
}: IRequestData): Promise<IRequestResponse> {
}: RequestData): Promise<RequestResponse> {
let response = null;
const endpointConfig = config || {};
let requestConfig = this.buildRequestConfig(
type,
url,
data,
endpointConfig
endpointConfig,
);

requestConfig = {
Expand Down
12 changes: 6 additions & 6 deletions src/types/api.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { EndpointConfig } from './http-request';

export declare type APIQueryParams = Record<string, any>;
export declare type APIUrlParams = Record<string, any>;
export declare type APIQueryParams = Record<string, unknown>;
export declare type APIUrlParams = Record<string, unknown>;
export declare type APIRequestConfig = EndpointConfig;
export declare type APIResponse = any;
export declare type APIResponse = unknown;

export declare type Endpoint<
Response = APIResponse,
QueryParamsOrData = APIQueryParams,
DynamicUrlParams = APIUrlParams
DynamicUrlParams = APIUrlParams,
> = <
ResponseData = Response,
QueryParams = QueryParamsOrData,
UrlParams = DynamicUrlParams
UrlParams = DynamicUrlParams,
>(
queryParams?: QueryParams | null,
urlParams?: UrlParams,
requestConfig?: EndpointConfig
requestConfig?: EndpointConfig,
) => Promise<ResponseData>;

export interface Endpoints {
Expand Down
20 changes: 12 additions & 8 deletions src/types/http-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,25 @@ import type {
AxiosResponse,
} from 'axios';

export type IRequestResponse<T = any> = Promise<AxiosResponse<T>>;
export type RequestResponse<T = unknown> = Promise<AxiosResponse<T>>;

export type ErrorHandlingStrategy =
| 'throwError'
| 'reject'
| 'silent'
| 'defaultResponse';

export type RequestError = AxiosError<any>;
export type RequestError = AxiosError<unknown>;

interface ErrorHandlerClass {
process(error?: RequestError): unknown;
}

type ErrorHandlerFunction = (error: RequestError) => any;
type ErrorHandlerFunction = (error: RequestError) => unknown;

export type EndpointsConfig<T extends string> = {
[K in T]: EndpointConfig;
};

export interface EndpointConfig extends AxiosRequestConfig {
cancellable?: boolean;
Expand All @@ -31,19 +35,19 @@ export interface EndpointConfig extends AxiosRequestConfig {
export interface RequestHandlerConfig extends EndpointConfig {
axios: AxiosStatic;
flattenResponse?: boolean;
defaultResponse?: any;
logger?: any;
defaultResponse?: unknown;
logger?: unknown;
onError?: ErrorHandlerFunction | ErrorHandlerClass;
}

export interface APIHandlerConfig extends RequestHandlerConfig {
apiUrl: string;
endpoints: Record<string, any>;
endpoints: Record<string, unknown>;
}

export interface IRequestData {
export interface RequestData {
type: string;
url: string;
data?: any;
data?: unknown;
config: EndpointConfig;
}
36 changes: 20 additions & 16 deletions test/api-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ApiHandler } from '../src/api-handler';
import { mockErrorCallbackClass } from './http-request-error-handler.spec';
import { endpoints, EndpointsList } from './mocks/endpoints';

type TestRequestHandler = Record<string, unknown>;

describe('API Handler', () => {
const apiUrl = 'http://example.com/api/';
const config = {
Expand Down Expand Up @@ -46,7 +48,9 @@ describe('API Handler', () => {

api.handleRequest = jest.fn().mockResolvedValueOnce(userDataMock);

const response = await api.getUserAddress({ userId: 1 });
const response = await api.getUserAddress({
userId: 1,
});

expect(api.handleRequest).not.toHaveBeenCalled();
expect(response).toBeNull();
Expand All @@ -57,7 +61,7 @@ describe('API Handler', () => {
it('should properly replace multiple URL params', async () => {
const api = new ApiHandler(config);

(api.requestHandler as any).get = jest
(api.requestHandler as unknown as TestRequestHandler).get = jest
.fn()
.mockResolvedValueOnce(userDataMock);

Expand All @@ -67,12 +71,12 @@ describe('API Handler', () => {
name: 'Mark',
});

expect((api.requestHandler as any).get).toHaveBeenCalledTimes(1);
expect((api.requestHandler as any).get).toHaveBeenCalledWith(
'/user-details/1/Mark',
{},
{}
);
expect(
(api.requestHandler as unknown as TestRequestHandler).get,
).toHaveBeenCalledTimes(1);
expect(
(api.requestHandler as unknown as TestRequestHandler).get,
).toHaveBeenCalledWith('/user-details/1/Mark', {}, {});
expect(response).toBe(userDataMock);
});

Expand All @@ -84,23 +88,23 @@ describe('API Handler', () => {
},
};

(api.requestHandler as any).get = jest
(api.requestHandler as unknown as TestRequestHandler).get = jest
.fn()
.mockResolvedValueOnce(userDataMock);

const endpoints = api as unknown as EndpointsList;
const response = await endpoints.getUserDetailsByIdAndName(
null,
{ id: 1, name: 'Mark' },
headers
headers,
);

expect((api.requestHandler as any).get).toHaveBeenCalledTimes(1);
expect((api.requestHandler as any).get).toHaveBeenCalledWith(
'/user-details/1/Mark',
{},
headers
);
expect(
(api.requestHandler as unknown as TestRequestHandler).get,
).toHaveBeenCalledTimes(1);
expect(
(api.requestHandler as unknown as TestRequestHandler).get,
).toHaveBeenCalledWith('/user-details/1/Mark', {}, headers);
expect(response).toBe(userDataMock);
});
});
Expand Down
Loading

0 comments on commit ab957fe

Please sign in to comment.