From cf2b892cb7dbded259d21b347790835c5f08434d Mon Sep 17 00:00:00 2001 From: jedmao Date: Fri, 30 Aug 2019 16:23:16 -0500 Subject: [PATCH 1/3] Improve TypeScript types --- package-lock.json | 6 +++ package.json | 1 + src/applyMiddleware.ts | 53 ++++++++------------------- src/createStore.ts | 8 +--- test/applyMiddleware.spec.ts | 65 ++++++++++++++++----------------- test/bindActionCreators.spec.ts | 6 +-- test/createStore.spec.ts | 7 ++-- test/helpers/actionCreators.ts | 46 ++++++++++++++++------- test/helpers/actionTypes.ts | 35 ++++++++++++++++++ test/helpers/middleware.ts | 17 +++++---- test/helpers/reducers.ts | 27 +++++--------- 11 files changed, 148 insertions(+), 123 deletions(-) diff --git a/package-lock.json b/package-lock.json index a481f01266..a325cc8684 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6201,6 +6201,12 @@ "util.promisify": "^1.0.0" } }, + "redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==", + "dev": true + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", diff --git a/package.json b/package.json index 69a3e8570a..996c3590dc 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "prettier": "^1.18.2", "rimraf": "^3.0.0", "rollup": "^1.20.3", + "redux-thunk": "^2.3.0", "rollup-plugin-babel": "^4.3.3", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-replace": "^2.2.0", diff --git a/src/applyMiddleware.ts b/src/applyMiddleware.ts index f735843252..4ac6984c5a 100644 --- a/src/applyMiddleware.ts +++ b/src/applyMiddleware.ts @@ -1,8 +1,10 @@ import compose from './compose' import { Middleware, MiddlewareAPI } from './types/middleware' -import { AnyAction } from './types/actions' -import { StoreEnhancer, StoreCreator, Dispatch } from './types/store' -import { Reducer } from './types/reducers' +import { + Dispatch, + StoreEnhancer, + StoreEnhancerStoreCreator +} from './types/store' /** * Creates a store enhancer that applies middleware to the dispatch method @@ -23,40 +25,17 @@ import { Reducer } from './types/reducers' * @template Ext Dispatch signature added by a middleware. * @template S The type of the state supported by a middleware. */ -export default function applyMiddleware(): StoreEnhancer -export default function applyMiddleware( - middleware1: Middleware -): StoreEnhancer<{ dispatch: Ext1 }> -export default function applyMiddleware( - middleware1: Middleware, - middleware2: Middleware -): StoreEnhancer<{ dispatch: Ext1 & Ext2 }> -export default function applyMiddleware( - middleware1: Middleware, - middleware2: Middleware, - middleware3: Middleware -): StoreEnhancer<{ dispatch: Ext1 & Ext2 & Ext3 }> -export default function applyMiddleware( - middleware1: Middleware, - middleware2: Middleware, - middleware3: Middleware, - middleware4: Middleware -): StoreEnhancer<{ dispatch: Ext1 & Ext2 & Ext3 & Ext4 }> -export default function applyMiddleware( - middleware1: Middleware, - middleware2: Middleware, - middleware3: Middleware, - middleware4: Middleware, - middleware5: Middleware -): StoreEnhancer<{ dispatch: Ext1 & Ext2 & Ext3 & Ext4 & Ext5 }> -export default function applyMiddleware( - ...middlewares: Middleware[] -): StoreEnhancer<{ dispatch: Ext }> -export default function applyMiddleware( - ...middlewares: Middleware[] -): StoreEnhancer { - return (createStore: StoreCreator) => ( - reducer: Reducer, +export default function applyMiddleware< + S = any, + M extends Middleware = Middleware +>( + ...middlewares: M[] +): StoreEnhancer< + M extends Middleware ? { dispatch: D } : never, + S +> { + return (createStore: StoreEnhancerStoreCreator) => ( + reducer, ...args: any[] ) => { const store = createStore(reducer, ...args) diff --git a/src/createStore.ts b/src/createStore.ts index a75fc2a304..b7d1e127c5 100644 --- a/src/createStore.ts +++ b/src/createStore.ts @@ -97,7 +97,7 @@ export default function createStore< throw new Error('Expected the reducer to be a function.') } - let currentReducer = reducer + let currentReducer: Reducer = reducer let currentState = preloadedState as S let currentListeners: (() => void)[] | null = [] let nextListeners = currentListeners @@ -273,11 +273,7 @@ export default function createStore< throw new Error('Expected the nextReducer to be a function.') } - // TODO: do this more elegantly - ;((currentReducer as unknown) as Reducer< - NewState, - NewActions - >) = nextReducer + currentReducer = nextReducer // This action has a similiar effect to ActionTypes.INIT. // Any reducers that existed in both the new and old rootReducer diff --git a/test/applyMiddleware.spec.ts b/test/applyMiddleware.spec.ts index fefd849b15..0e12e5b8c2 100644 --- a/test/applyMiddleware.spec.ts +++ b/test/applyMiddleware.spec.ts @@ -1,11 +1,19 @@ -import { createStore, applyMiddleware, Middleware, AnyAction, Action } from '..' +import { + createStore, + applyMiddleware, + Middleware, + AnyAction, + Action, + Store +} from '../src' +import { rootActions } from './helpers/actionTypes' import * as reducers from './helpers/reducers' import { addTodo, addTodoAsync, addTodoIfEmpty } from './helpers/actionCreators' import { thunk } from './helpers/middleware' describe('applyMiddleware', () => { it('warns when dispatching during middleware setup', () => { - function dispatchingMiddleware(store) { + function dispatchingMiddleware(store: Store) { store.dispatch(addTodo('Dont dispatch in middleware setup')) return next => action => next(action) } @@ -40,8 +48,8 @@ describe('applyMiddleware', () => { ]) }) - it('passes recursive dispatches through the middleware chain', () => { - function test(spyOnMethods) { + it('passes recursive dispatches through the middleware chain', async () => { + function test(spyOnMethods: jest.Mock) { return () => next => action => { spyOnMethods(action) return next(action) @@ -51,16 +59,11 @@ describe('applyMiddleware', () => { const spy = jest.fn() const store = applyMiddleware(test(spy), thunk)(createStore)(reducers.todos) - // the typing for redux-thunk is super complex, so we will use an as unknown hack - const dispatchedValue = (store.dispatch( - addTodoAsync('Use Redux') - ) as unknown) as Promise - return dispatchedValue.then(() => { - expect(spy.mock.calls.length).toEqual(2) - }) + await store.dispatch(addTodoAsync('Use Redux')) + expect(spy.mock.calls.length).toEqual(2) }) - it('works with thunk middleware', done => { + it('works with thunk middleware', async () => { const store = applyMiddleware(thunk)(createStore)(reducers.todos) store.dispatch(addTodoIfEmpty('Hello')) @@ -91,27 +94,21 @@ describe('applyMiddleware', () => { } ]) - // the typing for redux-thunk is super complex, so we will use an "as unknown" hack - const dispatchedValue = (store.dispatch( - addTodoAsync('Maybe') - ) as unknown) as Promise - dispatchedValue.then(() => { - expect(store.getState()).toEqual([ - { - id: 1, - text: 'Hello' - }, - { - id: 2, - text: 'World' - }, - { - id: 3, - text: 'Maybe' - } - ]) - done() - }) + await store.dispatch(addTodoAsync('Maybe')) + expect(store.getState()).toEqual([ + { + id: 1, + text: 'Hello' + }, + { + id: 2, + text: 'World' + }, + { + id: 3, + text: 'Maybe' + } + ]) }) it('passes through all arguments of dispatch calls from within middleware', () => { @@ -144,7 +141,7 @@ describe('applyMiddleware', () => { applyMiddleware(multiArgMiddleware, dummyMiddleware) ) - store.dispatch(spy) + store.dispatch(spy as any) expect(spy.mock.calls[0]).toEqual(testCallArgs) }) }) diff --git a/test/bindActionCreators.spec.ts b/test/bindActionCreators.spec.ts index 5efecb6902..ee7bfac8f4 100644 --- a/test/bindActionCreators.spec.ts +++ b/test/bindActionCreators.spec.ts @@ -1,10 +1,10 @@ -import { bindActionCreators, createStore, ActionCreator } from '..' +import { bindActionCreators, createStore, ActionCreator, Store } from '..' import { todos } from './helpers/reducers' import * as actionCreators from './helpers/actionCreators' describe('bindActionCreators', () => { - let store - let actionCreatorFunctions + let store: Store + let actionCreatorFunctions: Partial beforeEach(() => { store = createStore(todos) diff --git a/test/createStore.spec.ts b/test/createStore.spec.ts index 5e8ef1db50..a742bf532a 100644 --- a/test/createStore.spec.ts +++ b/test/createStore.spec.ts @@ -505,6 +505,7 @@ describe('createStore', () => { it('throws if action type is missing', () => { const store = createStore(reducers.todos) + // @ts-ignore expect(() => store.dispatch({})).toThrow( /Actions may not have an undefined "type" property/ ) @@ -519,10 +520,10 @@ describe('createStore', () => { it('does not throw if action type is falsy', () => { const store = createStore(reducers.todos) - expect(() => store.dispatch({ type: false })).not.toThrow() - expect(() => store.dispatch({ type: 0 })).not.toThrow() + expect(() => store.dispatch({ type: false } as any)).not.toThrow() + expect(() => store.dispatch({ type: 0 } as any)).not.toThrow() expect(() => store.dispatch({ type: null })).not.toThrow() - expect(() => store.dispatch({ type: '' })).not.toThrow() + expect(() => store.dispatch({ type: '' } as any)).not.toThrow() }) it('accepts enhancer as the third argument', () => { diff --git a/test/helpers/actionCreators.ts b/test/helpers/actionCreators.ts index 46964ea9ce..33dd57edc1 100644 --- a/test/helpers/actionCreators.ts +++ b/test/helpers/actionCreators.ts @@ -1,21 +1,29 @@ +import { ThunkAction } from 'redux-thunk' + import { ADD_TODO, + AddTodo, DISPATCH_IN_MIDDLE, + DispatchInMiddle, GET_STATE_IN_MIDDLE, + GetStateInMiddle, SUBSCRIBE_IN_MIDDLE, + SubscribeInMiddle, UNSUBSCRIBE_IN_MIDDLE, + UnsubscribeInMiddle, THROW_ERROR, - UNKNOWN_ACTION + ThrowError, + UNKNOWN_ACTION, + UnknownAction } from './actionTypes' -import { Action, AnyAction, Dispatch } from '../..' -export function addTodo(text: string): AnyAction { +export function addTodo(text: string): AddTodo { return { type: ADD_TODO, text } } -export function addTodoAsync(text: string) { - return (dispatch: Dispatch): Promise => - new Promise(resolve => +export function addTodoAsync(text: string): ThunkAction { + return dispatch => + new Promise(resolve => setImmediate(() => { dispatch(addTodo(text)) resolve() @@ -23,49 +31,59 @@ export function addTodoAsync(text: string) { ) } -export function addTodoIfEmpty(text: string) { - return (dispatch: Dispatch, getState: () => any) => { +export function addTodoIfEmpty( + text: string +): ThunkAction { + return (dispatch, getState) => { if (!getState().length) { dispatch(addTodo(text)) } } } -export function dispatchInMiddle(boundDispatchFn: () => void): AnyAction { +export function dispatchInMiddle( + boundDispatchFn: () => void +): DispatchInMiddle { return { type: DISPATCH_IN_MIDDLE, boundDispatchFn } } -export function getStateInMiddle(boundGetStateFn: () => void): AnyAction { +export function getStateInMiddle( + boundGetStateFn: () => void +): GetStateInMiddle { return { type: GET_STATE_IN_MIDDLE, boundGetStateFn } } -export function subscribeInMiddle(boundSubscribeFn: () => void): AnyAction { +export function subscribeInMiddle( + boundSubscribeFn: () => void +): SubscribeInMiddle { return { type: SUBSCRIBE_IN_MIDDLE, boundSubscribeFn } } -export function unsubscribeInMiddle(boundUnsubscribeFn: () => void): AnyAction { +export function unsubscribeInMiddle( + boundUnsubscribeFn: () => void +): UnsubscribeInMiddle { return { type: UNSUBSCRIBE_IN_MIDDLE, boundUnsubscribeFn } } -export function throwError(): Action { +export function throwError(): ThrowError { return { type: THROW_ERROR } } -export function unknownAction(): Action { +export function unknownAction(): UnknownAction { return { type: UNKNOWN_ACTION } diff --git a/test/helpers/actionTypes.ts b/test/helpers/actionTypes.ts index 2e6104345c..a0f20f4ce3 100644 --- a/test/helpers/actionTypes.ts +++ b/test/helpers/actionTypes.ts @@ -1,7 +1,42 @@ +import { Action } from '../..' + export const ADD_TODO = 'ADD_TODO' +export interface AddTodo extends Action { + text: string +} + export const DISPATCH_IN_MIDDLE = 'DISPATCH_IN_MIDDLE' +export interface DispatchInMiddle extends Action { + boundDispatchFn: () => void +} + export const GET_STATE_IN_MIDDLE = 'GET_STATE_IN_MIDDLE' +export interface GetStateInMiddle extends Action { + boundGetStateFn: () => void +} + export const SUBSCRIBE_IN_MIDDLE = 'SUBSCRIBE_IN_MIDDLE' +export interface SubscribeInMiddle extends Action { + boundSubscribeFn: () => void +} + export const UNSUBSCRIBE_IN_MIDDLE = 'UNSUBSCRIBE_IN_MIDDLE' +export interface UnsubscribeInMiddle + extends Action { + boundUnsubscribeFn: () => void +} + export const THROW_ERROR = 'THROW_ERROR' +export type ThrowError = Action + export const UNKNOWN_ACTION = 'UNKNOWN_ACTION' +export type UnknownAction = Action + +export type rootActions = + | AddTodo + | DispatchInMiddle + | GetStateInMiddle + | SubscribeInMiddle + | UnsubscribeInMiddle + | ThrowError + | UnknownAction diff --git a/test/helpers/middleware.ts b/test/helpers/middleware.ts index 5bf51a6754..6400e6a915 100644 --- a/test/helpers/middleware.ts +++ b/test/helpers/middleware.ts @@ -1,12 +1,13 @@ -import { MiddlewareAPI, Dispatch, AnyAction } from '../..' +import { ThunkDispatch } from 'redux-thunk' -type ThunkAction = T extends AnyAction - ? AnyAction - : T extends Function - ? T - : never +import { rootActions } from './actionTypes' +import { Middleware } from '../..' -export function thunk({ dispatch, getState }: MiddlewareAPI) { - return (next: Dispatch) => (action: ThunkAction) => +export const thunk: Middleware< + ThunkDispatch, + any, + ThunkDispatch +> = ({ dispatch, getState }) => { + return next => action => typeof action === 'function' ? action(dispatch, getState) : next(action) } diff --git a/test/helpers/reducers.ts b/test/helpers/reducers.ts index c10d3d017e..a261d20bd1 100644 --- a/test/helpers/reducers.ts +++ b/test/helpers/reducers.ts @@ -3,10 +3,10 @@ import { DISPATCH_IN_MIDDLE, GET_STATE_IN_MIDDLE, SUBSCRIBE_IN_MIDDLE, + THROW_ERROR, UNSUBSCRIBE_IN_MIDDLE, - THROW_ERROR + rootActions } from './actionTypes' -import { AnyAction } from '../..' function id(state: { id: number }[] = []) { return ( @@ -18,9 +18,8 @@ export interface Todo { id: number text: string } -export type TodoAction = { type: 'ADD_TODO'; text: string } | AnyAction -export function todos(state: Todo[] = [], action: TodoAction) { +export function todos(state: Todo[] = [], action: rootActions) { switch (action.type) { case ADD_TODO: return [ @@ -35,7 +34,7 @@ export function todos(state: Todo[] = [], action: TodoAction) { } } -export function todosReverse(state: Todo[] = [], action: TodoAction) { +export function todosReverse(state: Todo[] = [], action: rootActions) { switch (action.type) { case ADD_TODO: return [ @@ -52,9 +51,7 @@ export function todosReverse(state: Todo[] = [], action: TodoAction) { export function dispatchInTheMiddleOfReducer( state = [], - action: - | { type: 'DISPATCH_IN_MIDDLE'; boundDispatchFn: () => void } - | AnyAction + action: rootActions ) { switch (action.type) { case DISPATCH_IN_MIDDLE: @@ -67,9 +64,7 @@ export function dispatchInTheMiddleOfReducer( export function getStateInTheMiddleOfReducer( state = [], - action: - | { type: 'DISPATCH_IN_MIDDLE'; boundGetStateFn: () => void } - | AnyAction + action: rootActions ) { switch (action.type) { case GET_STATE_IN_MIDDLE: @@ -82,9 +77,7 @@ export function getStateInTheMiddleOfReducer( export function subscribeInTheMiddleOfReducer( state = [], - action: - | { type: 'DISPATCH_IN_MIDDLE'; boundSubscribeFn: () => void } - | AnyAction + action: rootActions ) { switch (action.type) { case SUBSCRIBE_IN_MIDDLE: @@ -97,9 +90,7 @@ export function subscribeInTheMiddleOfReducer( export function unsubscribeInTheMiddleOfReducer( state = [], - action: - | { type: 'DISPATCH_IN_MIDDLE'; boundUnsubscribeFn: () => void } - | AnyAction + action: rootActions ) { switch (action.type) { case UNSUBSCRIBE_IN_MIDDLE: @@ -110,7 +101,7 @@ export function unsubscribeInTheMiddleOfReducer( } } -export function errorThrowingReducer(state = [], action: AnyAction) { +export function errorThrowingReducer(state = [], action: rootActions) { switch (action.type) { case THROW_ERROR: throw new Error() From 1d5e80e495be89ee09e2864902bf5c3edcbf6b4a Mon Sep 17 00:00:00 2001 From: Jed Mao Date: Sun, 29 Sep 2019 21:46:19 -0500 Subject: [PATCH 2/3] npm run format --- test/helpers/reducers.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/test/helpers/reducers.ts b/test/helpers/reducers.ts index a261d20bd1..433d4d9337 100644 --- a/test/helpers/reducers.ts +++ b/test/helpers/reducers.ts @@ -49,10 +49,7 @@ export function todosReverse(state: Todo[] = [], action: rootActions) { } } -export function dispatchInTheMiddleOfReducer( - state = [], - action: rootActions -) { +export function dispatchInTheMiddleOfReducer(state = [], action: rootActions) { switch (action.type) { case DISPATCH_IN_MIDDLE: action.boundDispatchFn() @@ -62,10 +59,7 @@ export function dispatchInTheMiddleOfReducer( } } -export function getStateInTheMiddleOfReducer( - state = [], - action: rootActions -) { +export function getStateInTheMiddleOfReducer(state = [], action: rootActions) { switch (action.type) { case GET_STATE_IN_MIDDLE: action.boundGetStateFn() @@ -75,10 +69,7 @@ export function getStateInTheMiddleOfReducer( } } -export function subscribeInTheMiddleOfReducer( - state = [], - action: rootActions -) { +export function subscribeInTheMiddleOfReducer(state = [], action: rootActions) { switch (action.type) { case SUBSCRIBE_IN_MIDDLE: action.boundSubscribeFn() From ebe74235eaf0654a2745f0bba8d4d728ad3bd7ee Mon Sep 17 00:00:00 2001 From: Jed Mao Date: Sun, 29 Sep 2019 22:15:39 -0500 Subject: [PATCH 3/3] Restore multi-ext types in applyMiddleware --- src/applyMiddleware.ts | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/applyMiddleware.ts b/src/applyMiddleware.ts index 4ac6984c5a..ab077db06b 100644 --- a/src/applyMiddleware.ts +++ b/src/applyMiddleware.ts @@ -25,15 +25,38 @@ import { * @template Ext Dispatch signature added by a middleware. * @template S The type of the state supported by a middleware. */ -export default function applyMiddleware< - S = any, - M extends Middleware = Middleware ->( - ...middlewares: M[] -): StoreEnhancer< - M extends Middleware ? { dispatch: D } : never, - S -> { +export default function applyMiddleware(): StoreEnhancer<{ dispatch: {} }, {}> +export default function applyMiddleware( + middleware1: Middleware +): StoreEnhancer<{ dispatch: Ext1 }, S> +export default function applyMiddleware( + middleware1: Middleware, + middleware2: Middleware +): StoreEnhancer<{ dispatch: Ext1 & Ext2 }, S> +export default function applyMiddleware( + middleware1: Middleware, + middleware2: Middleware, + middleware3: Middleware +): StoreEnhancer<{ dispatch: Ext1 & Ext2 & Ext3 }, S> +export default function applyMiddleware( + middleware1: Middleware, + middleware2: Middleware, + middleware3: Middleware, + middleware4: Middleware +): StoreEnhancer<{ dispatch: Ext1 & Ext2 & Ext3 & Ext4 }, S> +export default function applyMiddleware( + middleware1: Middleware, + middleware2: Middleware, + middleware3: Middleware, + middleware4: Middleware, + middleware5: Middleware +): StoreEnhancer<{ dispatch: Ext1 & Ext2 & Ext3 & Ext4 & Ext5 }, S> +export default function applyMiddleware( + ...middlewares: Middleware[] +): StoreEnhancer<{ dispatch: Ext }, S> +export default function applyMiddleware( + ...middlewares: Middleware[] +): StoreEnhancer { return (createStore: StoreEnhancerStoreCreator) => ( reducer, ...args: any[]