Skip to content

Commit

Permalink
Merge pull request #34 from dabapps/generic-returns
Browse files Browse the repository at this point in the history
Set generic types for requests and reducers
  • Loading branch information
isidornygren authored Mar 4, 2020
2 parents 8818b41 + e8b77de commit 8a65336
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 49 deletions.
5 changes: 1 addition & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ sudo: false

language: node_js

node_js:
- 6.11.5

install:
- npm install
- npm ci

script:
- npm test
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dabapps/redux-requests",
"version": "0.5.5",
"version": "0.5.6",
"description": "Library for simple redux requests",
"main": "dist/js/index.js",
"directories": {},
Expand Down
10 changes: 5 additions & 5 deletions src/ts/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function serializeMeta(meta: Partial<ExtraMeta>, options: Options): ExtraMeta {
};
}

export function requestWithConfig(
export function requestWithConfig<T = {}>(
actionSet: AsyncActionSet,
axoisConfig: AxiosRequestConfig,
options: Options = {},
Expand All @@ -59,8 +59,8 @@ export function requestWithConfig(
dispatch({ type: actionSet.REQUEST, meta });
dispatch(setRequestState(actionSet, 'REQUEST', null, meta.tag));

return apiRequest(axoisConfig).then(
(response: AxiosResponse) => {
return apiRequest<T>(axoisConfig).then(
(response: AxiosResponse<T>) => {
dispatch({
type: actionSet.SUCCESS,
payload: response,
Expand Down Expand Up @@ -90,15 +90,15 @@ export function requestWithConfig(
};
}

export function request(
export function request<T = {}>(
actionSet: AsyncActionSet,
url: string,
method: UrlMethod,
data?: string | number | Dict<any> | ReadonlyArray<any>,
params: RequestParams = {}
) {
const { headers, tag, metaData, shouldRethrow } = params;
return requestWithConfig(
return requestWithConfig<T>(
actionSet,
{ url, method, data, headers },
{ tag, shouldRethrow },
Expand Down
6 changes: 3 additions & 3 deletions src/ts/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {
SetRequestStatePayload,
} from './types';

export function responsesReducer(
state: ResponsesReducerState = {},
export function responsesReducer<T = any>(
state: ResponsesReducerState<T> = {},
action: AnyAction
): ResponsesReducerState {
): ResponsesReducerState<T> {
switch (action.type) {
case REQUEST_STATE:
if (isFSA(action)) {
Expand Down
6 changes: 3 additions & 3 deletions src/ts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ export type AsyncActionSet = Readonly<{
SUCCESS: string;
}>;

export type ResponseState = Readonly<{
export type ResponseState<T = {}> = Readonly<{
requestState: RequestStates | null;
data: AxiosResponse<any> | AxiosError | null;
data: AxiosResponse<T> | AxiosError | null;
}>;

export type ResponsesReducerState = Dict<Dict<ResponseState>>;
export type ResponsesReducerState<T = any> = Dict<Dict<ResponseState<T>>>;

export type SetRequestStatePayload = Readonly<{
actionSet: AsyncActionSet;
Expand Down
39 changes: 23 additions & 16 deletions src/ts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,20 @@ export function makeAsyncActionSet(actionName: string): AsyncActionSet {
};
}

export function formatQueryParams<T>(params?: Dict<T>): string {
export function formatQueryParams(
params?: Dict<string | number | boolean | null | undefined>
): string {
if (!params) {
return '';
}

const asPairs = asEntries(params);
const filteredPairs = asPairs
.filter(
([, value]: [string, T]) => value !== null && typeof value !== 'undefined'
<T>(
tuple: [string, T]
): tuple is [string, Exclude<T, undefined | null>] =>
tuple[1] !== null && typeof tuple[1] !== 'undefined'
)
.map(([key, value]) => [key, value.toString()]);

Expand All @@ -45,7 +50,9 @@ export function formatQueryParams<T>(params?: Dict<T>): string {
return '?' + filteredPairs.map(([key, value]) => `${key}=${value}`).join('&');
}

export function apiRequest(options: AxiosRequestConfig): AxiosPromise {
export function apiRequest<T = {}>(
options: AxiosRequestConfig
): AxiosPromise<T> {
const combinedHeaders = {
Accept: 'application/json',
'Content-Type': 'application/json',
Expand Down Expand Up @@ -82,39 +89,39 @@ export function apiRequest(options: AxiosRequestConfig): AxiosPromise {
return axios(config);
}

function getResponseState(
state: ResponsesReducerState,
function getResponseState<T>(
state: ResponsesReducerState<T>,
actionSet: AsyncActionSet,
tag?: string
): ResponseState {
): ResponseState<T> {
return (state[actionSet.REQUEST] || {})[tag || ''] || {};
}
export function isPending(
state: ResponsesReducerState,
export function isPending<T>(
state: ResponsesReducerState<T>,
actionSet: AsyncActionSet,
tag?: string
): boolean {
return getResponseState(state, actionSet, tag).requestState === 'REQUEST';
}

export function hasFailed(
state: ResponsesReducerState,
export function hasFailed<T>(
state: ResponsesReducerState<T>,
actionSet: AsyncActionSet,
tag?: string
): boolean {
return getResponseState(state, actionSet, tag).requestState === 'FAILURE';
}

export function hasSucceeded(
state: ResponsesReducerState,
export function hasSucceeded<T>(
state: ResponsesReducerState<T>,
actionSet: AsyncActionSet,
tag?: string
): boolean {
return getResponseState(state, actionSet, tag).requestState === 'SUCCESS';
}

export function anyPending(
state: ResponsesReducerState,
export function anyPending<T>(
state: ResponsesReducerState<T>,
actionSets: ReadonlyArray<AsyncActionSet | [AsyncActionSet, string]>
): boolean {
return actionSets.some(actionSet => {
Expand All @@ -131,8 +138,8 @@ function isAxiosError(data: Dict<any>): data is AxiosError {
return 'config' in data && 'name' in data && 'message' in data;
}

export function getErrorData(
state: ResponsesReducerState,
export function getErrorData<T>(
state: ResponsesReducerState<T>,
actionSet: AsyncActionSet,
tag?: string
): AxiosError | null {
Expand Down
45 changes: 29 additions & 16 deletions tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,18 @@ describe('Requests', () => {
expect(requestWithLotsOfParams).not.toThrowError();
});

it('should have sensible return types', () => {
request<{ date: number; name: string }>(
ACTION_SET,
'/api/llama/',
METHOD
)(dispatch).then(response => {
if (response && response.data.name === 'name') {
// Just checks the typechecker here, not necessary to jest test it
}
});
});

it('should allow for Header overrides', () => {
const headerThunk = request(
ACTION_SET,
Expand All @@ -152,7 +164,7 @@ describe('Requests', () => {
{},
{ headers: { header1: 'blah' }, tag: 'tag' }
);
myRequest = (headerThunk(dispatch) as any) as AxiosMock;
myRequest = (headerThunk(dispatch) as unknown) as AxiosMock;
expect(
myRequest.params.headers && myRequest.params.headers.header1
).toBe('blah');
Expand All @@ -161,8 +173,9 @@ describe('Requests', () => {
it('should return a thunk for sending a generic request', () => {
expect(typeof thunk).toBe('function');
});

it('should dispatch request actions', () => {
myRequest = (thunk(dispatch) as any) as AxiosMock; // FIXME: We need type-safe mocking
myRequest = (thunk(dispatch) as unknown) as AxiosMock;

expect(dispatch).toHaveBeenCalledWith({
meta: {
Expand All @@ -177,21 +190,21 @@ describe('Requests', () => {
});

it('should normalize URLs', () => {
myRequest = request(ACTION_SET, '/api//llama/', METHOD)(
myRequest = (request(ACTION_SET, '/api//llama/', METHOD)(
dispatch
) as any;
expect((myRequest as any).params.url).toEqual('/api/llama/');
) as unknown) as AxiosMock;
expect(myRequest.params.url).toEqual('/api/llama/');
});

it('should not normalize absolute URLs', () => {
myRequest = request(ACTION_SET, 'http://www.test.com', METHOD)(
myRequest = (request(ACTION_SET, 'http://www.test.com', METHOD)(
dispatch
) as any;
expect((myRequest as any).params.url).toEqual('http://www.test.com');
) as unknown) as AxiosMock;
expect(myRequest.params.url).toEqual('http://www.test.com');
});

it('should dispatch success actions', () => {
myRequest = (thunk(dispatch) as any) as AxiosMock;
myRequest = (thunk(dispatch) as unknown) as AxiosMock;
myRequest.success({
data: 'llama',
});
Expand All @@ -210,7 +223,7 @@ describe('Requests', () => {
});

it('should dispatch failure actions', () => {
myRequest = (thunk(dispatch) as any) as AxiosMock;
myRequest = (thunk(dispatch) as unknown) as AxiosMock;
const result = myRequest.failure({
response: {
data: 'llama',
Expand Down Expand Up @@ -274,18 +287,18 @@ describe('Requests', () => {
expect(typeof thunk).toBe('function');
});
it('should set url', () => {
const myRequest = requestWithConfig(ACTION_SET, {
const myRequest = (requestWithConfig(ACTION_SET, {
url: 'http://www.test.com',
method: METHOD,
})(dispatch) as any;
expect((myRequest as any).params.url).toEqual('http://www.test.com');
})(dispatch) as unknown) as AxiosMock;
expect(myRequest.params.url).toEqual('http://www.test.com');
});
it('should set method', () => {
const myRequest = requestWithConfig(ACTION_SET, {
const myRequest = (requestWithConfig(ACTION_SET, {
url: 'http://www.test.com',
method: METHOD,
})(dispatch) as any;
expect((myRequest as any).params.method).toEqual(METHOD);
})(dispatch) as unknown) as AxiosMock;
expect(myRequest.params.method).toEqual(METHOD);
});
it('should take extra meta but not override the tag', () => {
requestWithConfig(
Expand Down

0 comments on commit 8a65336

Please sign in to comment.