From ac804a58126d778e19e830ad7a7732bd6b57437d Mon Sep 17 00:00:00 2001 From: Nathan Bierema Date: Sun, 26 Mar 2023 18:19:09 -0400 Subject: [PATCH] Add PreloadedState generic --- src/applyMiddleware.ts | 44 +++++++++------------ src/combineReducers.ts | 34 ++++++++--------- src/createStore.ts | 37 ++++++++++-------- src/index.ts | 7 ++-- src/types/reducers.ts | 74 +++++++++++++++++++++++++++--------- src/types/store.ts | 62 +++++++----------------------- test/combineReducers.spec.ts | 32 ++++++++++------ test/createStore.spec.ts | 24 ++++++++---- test/typescript/enhancers.ts | 61 +++++++++++++++++------------ test/typescript/reducers.ts | 2 +- test/typescript/store.ts | 67 +++++++++++++++++++++++++++++++- 11 files changed, 268 insertions(+), 176 deletions(-) diff --git a/src/applyMiddleware.ts b/src/applyMiddleware.ts index 744ed27895..2f1b4752e9 100644 --- a/src/applyMiddleware.ts +++ b/src/applyMiddleware.ts @@ -1,8 +1,6 @@ import compose from './compose' import { Middleware, MiddlewareAPI } from './types/middleware' -import { AnyAction } from './types/actions' -import { StoreEnhancer, Dispatch, PreloadedState } from './types/store' -import { Reducer } from './types/reducers' +import { StoreEnhancer, Dispatch } from './types/store' /** * Creates a store enhancer that applies middleware to the dispatch method @@ -55,29 +53,25 @@ export default function applyMiddleware( export default function applyMiddleware( ...middlewares: Middleware[] ): StoreEnhancer { - return createStore => - ( - reducer: Reducer, - preloadedState?: PreloadedState - ) => { - const store = createStore(reducer, preloadedState) - let dispatch: Dispatch = () => { - throw new Error( - 'Dispatching while constructing your middleware is not allowed. ' + - 'Other middleware would not be applied to this dispatch.' - ) - } + return createStore => (reducer, preloadedState) => { + const store = createStore(reducer, preloadedState) + let dispatch: Dispatch = () => { + throw new Error( + 'Dispatching while constructing your middleware is not allowed. ' + + 'Other middleware would not be applied to this dispatch.' + ) + } - const middlewareAPI: MiddlewareAPI = { - getState: store.getState, - dispatch: (action, ...args) => dispatch(action, ...args) - } - const chain = middlewares.map(middleware => middleware(middlewareAPI)) - dispatch = compose(...chain)(store.dispatch) + const middlewareAPI: MiddlewareAPI = { + getState: store.getState, + dispatch: (action, ...args) => dispatch(action, ...args) + } + const chain = middlewares.map(middleware => middleware(middlewareAPI)) + dispatch = compose(...chain)(store.dispatch) - return { - ...store, - dispatch - } + return { + ...store, + dispatch } + } } diff --git a/src/combineReducers.ts b/src/combineReducers.ts index ee005cee88..2894f97c4e 100644 --- a/src/combineReducers.ts +++ b/src/combineReducers.ts @@ -1,11 +1,10 @@ import { AnyAction, Action } from './types/actions' import { ActionFromReducersMapObject, + PreloadedStateShapeFromReducersMapObject, Reducer, - ReducersMapObject, StateFromReducersMapObject } from './types/reducers' -import { CombinedState } from './types/store' import ActionTypes from './utils/actionTypes' import isPlainObject from './utils/isPlainObject' @@ -14,7 +13,7 @@ import { kindOf } from './utils/kindOf' function getUnexpectedStateShapeWarningMessage( inputState: object, - reducers: ReducersMapObject, + reducers: { [key: string]: Reducer }, action: Action, unexpectedKeyCache: { [key: string]: true } ) { @@ -60,7 +59,9 @@ function getUnexpectedStateShapeWarningMessage( } } -function assertReducerShape(reducers: ReducersMapObject) { +function assertReducerShape(reducers: { + [key: string]: Reducer +}) { Object.keys(reducers).forEach(key => { const reducer = reducers[key] const initialState = reducer(undefined, { type: ActionTypes.INIT }) @@ -110,21 +111,20 @@ function assertReducerShape(reducers: ReducersMapObject) { * @returns A reducer function that invokes every reducer inside the passed * object, and builds a state object with the same shape. */ -export default function combineReducers( - reducers: ReducersMapObject -): Reducer> -export default function combineReducers( - reducers: ReducersMapObject -): Reducer, A> -export default function combineReducers( +export default function combineReducers( reducers: M -): Reducer< - CombinedState>, - ActionFromReducersMapObject -> -export default function combineReducers(reducers: ReducersMapObject) { +): M[keyof M] extends Reducer | undefined + ? Reducer< + StateFromReducersMapObject, + ActionFromReducersMapObject, + Partial> + > + : never +export default function combineReducers(reducers: { + [key: string]: Reducer +}) { const reducerKeys = Object.keys(reducers) - const finalReducers: ReducersMapObject = {} + const finalReducers: { [key: string]: Reducer } = {} for (let i = 0; i < reducerKeys.length; i++) { const key = reducerKeys[i] diff --git a/src/createStore.ts b/src/createStore.ts index 921b723c5f..f3b8ba9577 100644 --- a/src/createStore.ts +++ b/src/createStore.ts @@ -2,7 +2,6 @@ import $$observable from './utils/symbol-observable' import { Store, - PreloadedState, StoreEnhancer, Dispatch, Observer, @@ -77,20 +76,22 @@ export function createStore< S, A extends Action, Ext extends {} = {}, - StateExt extends {} = {} + StateExt extends {} = {}, + PreloadedState = S >( - reducer: Reducer, - preloadedState?: PreloadedState, + reducer: Reducer, + preloadedState?: PreloadedState | undefined, enhancer?: StoreEnhancer ): Store & Ext export function createStore< S, A extends Action, Ext extends {} = {}, - StateExt extends {} = {} + StateExt extends {} = {}, + PreloadedState = S >( - reducer: Reducer, - preloadedState?: PreloadedState | StoreEnhancer, + reducer: Reducer, + preloadedState?: PreloadedState | StoreEnhancer | undefined, enhancer?: StoreEnhancer ): Store & Ext { if (typeof reducer !== 'function') { @@ -128,12 +129,14 @@ export function createStore< return enhancer(createStore)( reducer, - preloadedState as PreloadedState - ) as Store & Ext + preloadedState as PreloadedState | undefined + ) } let currentReducer = reducer - let currentState = preloadedState as S + let currentState: S | PreloadedState | undefined = preloadedState as + | PreloadedState + | undefined let currentListeners: Map | null = new Map() let nextListeners = currentListeners let listenerIdCounter = 0 @@ -315,7 +318,7 @@ export function createStore< ) } - currentReducer = nextReducer + currentReducer = nextReducer as unknown as Reducer // This action has a similar effect to ActionTypes.INIT. // Any reducers that existed in both the new and old rootReducer @@ -456,20 +459,22 @@ export function legacy_createStore< S, A extends Action, Ext extends {} = {}, - StateExt extends {} = {} + StateExt extends {} = {}, + PreloadedState = S >( - reducer: Reducer, - preloadedState?: PreloadedState, + reducer: Reducer, + preloadedState?: PreloadedState | undefined, enhancer?: StoreEnhancer ): Store & Ext export function legacy_createStore< S, A extends Action, Ext extends {} = {}, - StateExt extends {} = {} + StateExt extends {} = {}, + PreloadedState = S >( reducer: Reducer, - preloadedState?: PreloadedState | StoreEnhancer, + preloadedState?: PreloadedState | StoreEnhancer | undefined, enhancer?: StoreEnhancer ): Store & Ext { return createStore(reducer, preloadedState as any, enhancer) diff --git a/src/index.ts b/src/index.ts index 0fa083343b..ef3a1791ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,8 +9,6 @@ import __DO_NOT_USE__ActionTypes from './utils/actionTypes' // types // store export { - CombinedState, - PreloadedState, Dispatch, Unsubscribe, Observable, @@ -23,11 +21,12 @@ export { // reducers export { Reducer, - ReducerFromReducersMapObject, ReducersMapObject, StateFromReducersMapObject, + ReducerFromReducersMapObject, ActionFromReducer, - ActionFromReducersMapObject + ActionFromReducersMapObject, + PreloadedStateShapeFromReducersMapObject } from './types/reducers' // action creators export { ActionCreator, ActionCreatorsMapObject } from './types/actions' diff --git a/src/types/reducers.ts b/src/types/reducers.ts index cd084f203a..7fd31b3c49 100644 --- a/src/types/reducers.ts +++ b/src/types/reducers.ts @@ -25,28 +25,46 @@ import { Action, AnyAction } from './actions' * * @template S The type of state consumed and produced by this reducer. * @template A The type of actions the reducer can potentially respond to. + * @template PreloadedState The type of state consumed by this reducer the first time it's called. */ -export type Reducer = ( - state: S | undefined, - action: A -) => S +export type Reducer< + S = any, + A extends Action = AnyAction, + PreloadedState = S +> = (state: S | PreloadedState | undefined, action: A) => S /** * Object whose values correspond to different reducer functions. * + * @template S The combined state of the reducers. * @template A The type of actions the reducers can potentially respond to. + * @template PreloadedState The combined preloaded state of the reducers. */ -export type ReducersMapObject = { - [K in keyof S]: Reducer -} +export type ReducersMapObject< + S = any, + A extends Action = AnyAction, + PreloadedState = S +> = keyof PreloadedState extends keyof S + ? { + [K in keyof S]: Reducer< + S[K], + A, + K extends keyof PreloadedState ? PreloadedState[K] : never + > + } + : never /** * Infer a combined state shape from a `ReducersMapObject`. * * @template M Object map of reducers as provided to `combineReducers(map: M)`. */ -export type StateFromReducersMapObject = M extends ReducersMapObject - ? { [P in keyof M]: M[P] extends Reducer ? S : never } +export type StateFromReducersMapObject = M[keyof M] extends + | Reducer + | undefined + ? { + [P in keyof M]: M[P] extends Reducer ? S : never + } : never /** @@ -54,12 +72,10 @@ export type StateFromReducersMapObject = M extends ReducersMapObject * * @template M Object map of reducers as provided to `combineReducers(map: M)`. */ -export type ReducerFromReducersMapObject = M extends { - [P in keyof M]: infer R -} - ? R extends Reducer - ? R - : never +export type ReducerFromReducersMapObject = M[keyof M] extends + | Reducer + | undefined + ? M[keyof M] : never /** @@ -67,13 +83,35 @@ export type ReducerFromReducersMapObject = M extends { * * @template R Type of reducer. */ -export type ActionFromReducer = R extends Reducer ? A : never +export type ActionFromReducer = R extends + | Reducer + | undefined + ? A + : never /** * Infer action union type from a `ReducersMapObject`. * * @template M Object map of reducers as provided to `combineReducers(map: M)`. */ -export type ActionFromReducersMapObject = M extends ReducersMapObject - ? ActionFromReducer> +export type ActionFromReducersMapObject = ActionFromReducer< + ReducerFromReducersMapObject +> + +/** + * Infer a combined preloaded state shape from a `ReducersMapObject`. + * + * @template M Object map of reducers as provided to `combineReducers(map: M)`. + */ +export type PreloadedStateShapeFromReducersMapObject = M[keyof M] extends + | Reducer + | undefined + ? { + [P in keyof M]: M[P] extends ( + inputState: infer InputState, + action: AnyAction + ) => any + ? InputState + : never + } : never diff --git a/src/types/store.ts b/src/types/store.ts index f071dcb136..e33d783005 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -2,49 +2,6 @@ import { Action, AnyAction } from './actions' import { Reducer } from './reducers' import '../utils/symbol-observable' -/** - * Internal "virtual" symbol used to make the `CombinedState` type unique. - */ -declare const $CombinedState: unique symbol - -/** - * State base type for reducers created with `combineReducers()`. - * - * This type allows the `createStore()` method to infer which levels of the - * preloaded state can be partial. - * - * Because Typescript is really duck-typed, a type needs to have some - * identifying property to differentiate it from other types with matching - * prototypes for type checking purposes. That's why this type has the - * `$CombinedState` symbol property. Without the property, this type would - * match any object. The symbol doesn't really exist because it's an internal - * (i.e. not exported), and internally we never check its value. Since it's a - * symbol property, it's not expected to be unumerable, and the value is - * typed as always undefined, so its never expected to have a meaningful - * value anyway. It just makes this type distinguishable from plain `{}`. - */ -interface EmptyObject { - readonly [$CombinedState]?: undefined -} -export type CombinedState = EmptyObject & S - -/** - * Recursively makes combined state objects partial. Only combined state _root - * objects_ (i.e. the generated higher level object with keys mapping to - * individual reducers) are partial. - */ -export type PreloadedState = Required extends EmptyObject - ? S extends CombinedState - ? { - [K in keyof S1]?: S1[K] extends object ? PreloadedState : S1[K] - } - : S - : { - [K in keyof S]: S[K] extends string | number | boolean | symbol - ? S[K] - : PreloadedState - } - /** * A *dispatching function* (or simply *dispatch function*) is a function that * accepts an action or an async action; it then may or may not dispatch one @@ -214,6 +171,7 @@ export interface Store< * * @template S The type of state to be held by the store. * @template A The type of actions which may be dispatched. + * @template PreloadedState The initial state that is passed into the reducer. * @template Ext Store extension that is mixed in to the Store type. * @template StateExt State extension that is mixed into the state type. */ @@ -222,9 +180,15 @@ export interface StoreCreator { reducer: Reducer, enhancer?: StoreEnhancer ): Store & Ext - ( - reducer: Reducer, - preloadedState?: PreloadedState, + < + S, + A extends Action, + Ext extends {} = {}, + StateExt extends {} = {}, + PreloadedState = S + >( + reducer: Reducer, + preloadedState?: PreloadedState | undefined, enhancer?: StoreEnhancer ): Store & Ext } @@ -259,7 +223,7 @@ export type StoreEnhancer = < export type StoreEnhancerStoreCreator< Ext extends {} = {}, StateExt extends {} = {} -> = ( - reducer: Reducer, - preloadedState?: PreloadedState +> = ( + reducer: Reducer, + preloadedState?: PreloadedState | undefined ) => Store & Ext diff --git a/test/combineReducers.spec.ts b/test/combineReducers.spec.ts index 53dc6fb56a..356e72b52c 100644 --- a/test/combineReducers.spec.ts +++ b/test/combineReducers.spec.ts @@ -3,6 +3,7 @@ import { createStore, combineReducers, Reducer, + Action, AnyAction, __DO_NOT_USE__ActionTypes as ActionTypes } from 'redux' @@ -11,10 +12,13 @@ import { vi } from 'vitest' describe('Utils', () => { describe('combineReducers', () => { it('returns a composite reducer that maps the state keys to given reducers', () => { + type IncrementAction = { type: 'increment' } + type PushAction = { type: 'push'; value: unknown } + const reducer = combineReducers({ - counter: (state: number = 0, action) => + counter: (state: number = 0, action: IncrementAction) => action.type === 'increment' ? state + 1 : state, - stack: (state: any[] = [], action) => + stack: (state: any[] = [], action: PushAction) => action.type === 'push' ? [...state, action.value] : state }) @@ -50,7 +54,6 @@ describe('Utils', () => { ) spy.mockClear() - // @ts-expect-error combineReducers({ thing: undefined }) expect(spy.mock.calls[0][0]).toMatch( /No reducer provided for key "thing"/ @@ -62,7 +65,7 @@ describe('Utils', () => { it('throws an error if a reducer returns undefined handling an action', () => { const reducer = combineReducers({ - counter(state: number = 0, action) { + counter(state: number = 0, action: Action) { switch (action && action.type) { case 'increment': return state + 1 @@ -92,7 +95,7 @@ describe('Utils', () => { it('throws an error on first call if a reducer returns undefined initializing', () => { const reducer = combineReducers({ - counter(state: number, action) { + counter(state: number, action: Action) { switch (action.type) { case 'increment': return state + 1 @@ -123,7 +126,7 @@ describe('Utils', () => { const increment = Symbol('INCREMENT') const reducer = combineReducers({ - counter(state: number = 0, action) { + counter(state: number = 0, action: Action) { switch (action.type) { case increment: return state + 1 @@ -158,7 +161,10 @@ describe('Utils', () => { child1(state = {}) { return state }, - child2(state: { count: number } = { count: 0 }, action) { + child2( + state: { count: number } = { count: 0 }, + action: Action + ) { switch (action.type) { case 'increment': return { count: state.count + 1 } @@ -179,7 +185,7 @@ describe('Utils', () => { it('throws an error on first call if a reducer attempts to handle a private action', () => { const reducer = combineReducers({ - counter(state: number, action) { + counter(state: number, action: Action) { switch (action.type) { case 'increment': return state + 1 @@ -204,6 +210,7 @@ describe('Utils', () => { console.error = spy const reducer = combineReducers({}) + // @ts-ignore reducer(undefined, { type: '' }) expect(spy.mock.calls[0][0]).toMatch( /Store does not have a valid reducer/ @@ -224,7 +231,7 @@ describe('Utils', () => { baz: { qux: number } } - const reducer = combineReducers({ + const reducer = combineReducers({ foo(state = { bar: 1 }) { return state }, @@ -379,9 +386,10 @@ describe('Utils', () => { }) it('should return an updated state when one of more reducers passed to the combineReducers are removed', function () { - const originalCompositeReducer = combineReducers<{ foo?: {}; bar: {} }>( - { foo, bar } - ) + const originalCompositeReducer = combineReducers<{ + foo?: typeof foo + bar: typeof bar + }>({ foo, bar }) const store = createStore(originalCompositeReducer) store.dispatch(ACTION) diff --git a/test/createStore.spec.ts b/test/createStore.spec.ts index ec708b8d76..6259461d2b 100644 --- a/test/createStore.spec.ts +++ b/test/createStore.spec.ts @@ -3,7 +3,8 @@ import { combineReducers, StoreEnhancer, Action, - Store + Store, + Reducer } from 'redux' import { vi } from 'vitest' import { @@ -844,20 +845,27 @@ describe('createStore', () => { const originalConsoleError = console.error console.error = vi.fn() + const reducer: Reducer = (s = 0) => s + + const yReducer = combineReducers<{ + z: typeof reducer + w?: typeof reducer + }>({ + z: reducer, + w: reducer + }) + const store = createStore( - combineReducers<{ x?: number; y: { z: number; w?: number } }>({ - x: (s = 0, _) => s, - y: combineReducers({ - z: (s = 0, _) => s, - w: (s = 0, _) => s - }) + combineReducers<{ x?: typeof reducer; y: typeof yReducer }>({ + x: reducer, + y: yReducer }) ) store.replaceReducer( combineReducers({ y: combineReducers({ - z: (s = 0, _) => s + z: reducer }) }) ) diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts index 282d5166ed..9771d5ad68 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -15,9 +15,9 @@ function dispatchExtension() { dispatch: PromiseDispatch }> = createStore => - ( - reducer: Reducer, - preloadedState?: any + ( + reducer: Reducer, + preloadedState?: PreloadedState | undefined ) => { const store = createStore(reducer, preloadedState) return { @@ -48,16 +48,18 @@ function dispatchExtension() { */ function stateExtension() { interface ExtraState { - extraField: 'extra' + extraField: string } const enhancer: StoreEnhancer<{}, ExtraState> = createStore => - ( - reducer: Reducer, - preloadedState?: any + ( + reducer: Reducer, + preloadedState?: PreloadedState | undefined ) => { - function wrapReducer(reducer: Reducer): Reducer { + function wrapReducer( + reducer: Reducer + ): Reducer { return (state, action) => { const newState = reducer(state, action) return { @@ -116,16 +118,18 @@ function extraMethods() { */ function replaceReducerExtender() { interface ExtraState { - extraField: 'extra' + extraField: string } const enhancer: StoreEnhancer<{ method(): string }, ExtraState> = createStore => - ( - reducer: Reducer, - preloadedState?: any + ( + reducer: Reducer, + preloadedState?: PreloadedState | undefined ) => { - function wrapReducer(reducer: Reducer): Reducer { + function wrapReducer( + reducer: Reducer + ): Reducer { return (state, action) => { const newState = reducer(state, action) return { @@ -192,16 +196,20 @@ function mhelmersonExample() { function stateExtensionExpectedToWork() { interface ExtraState { - extraField: 'extra' + extraField: string } const enhancer: StoreEnhancer<{}, ExtraState> = createStore => - ( - reducer: Reducer, - preloadedState?: any + ( + reducer: Reducer, + preloadedState?: PreloadedState | undefined ) => { - const wrappedReducer: Reducer = (state, action) => { + const wrappedReducer: Reducer< + S & ExtraState, + A, + PreloadedState & ExtraState + > = (state, action) => { const newState = reducer(state, action) return { ...newState, @@ -268,11 +276,14 @@ function finalHelmersonExample() { foo: string } - function persistReducer>( + function persistReducer, PreloadedState>( config: any, - reducer: Reducer + reducer: Reducer ) { - return (state: (S & ExtraState) | undefined, action: A) => { + return ( + state: (S & ExtraState) | PreloadedState | undefined, + action: A + ) => { const newState = reducer(state, action) return { ...newState, @@ -289,11 +300,11 @@ function finalHelmersonExample() { persistConfig: any ): StoreEnhancer<{}, ExtraState> { return createStore => - >( - reducer: Reducer, - preloadedState?: any + , PreloadedState>( + reducer: Reducer, + preloadedState?: PreloadedState | undefined ) => { - const persistedReducer = persistReducer(persistConfig, reducer) + const persistedReducer = persistReducer(persistConfig, reducer) const store = createStore(persistedReducer, preloadedState) const persistor = persistStore(store) diff --git a/test/typescript/reducers.ts b/test/typescript/reducers.ts index f34ef34b1c..90a3b7742c 100644 --- a/test/typescript/reducers.ts +++ b/test/typescript/reducers.ts @@ -156,7 +156,7 @@ function discriminated() { combined(cs, { type: 'SOME_OTHER_TYPE' }) // Combined reducer can be made to only accept known actions. - const strictCombined = combineReducers<{ sub: State }, MyAction0>({ + const strictCombined = combineReducers({ sub: reducer0 }) diff --git a/test/typescript/store.ts b/test/typescript/store.ts index c491911e58..1cac024118 100644 --- a/test/typescript/store.ts +++ b/test/typescript/store.ts @@ -5,7 +5,8 @@ import { Action, StoreEnhancer, Unsubscribe, - Observer + Observer, + combineReducers } from 'redux' import 'symbol-observable' @@ -74,6 +75,70 @@ const storeWithBadPreloadedState: Store = createStore(reducer, { e: brandedString }) +const reducerA: Reducer = (state = 'a') => state +const reducerB: Reducer<{ c: string; d: string }> = ( + state = { c: 'c', d: 'd' } +) => state +const reducerE: Reducer = (state = brandedString) => state + +const combinedReducer = combineReducers({ + a: reducerA, + b: reducerB, + e: reducerE +}) + +const storeWithCombineReducer = createStore(combinedReducer, { + b: { c: 'c', d: 'd' }, + e: brandedString +}) +// @ts-expect-error +const storeWithCombineReducerAndBadPreloadedState = createStore( + combinedReducer, + { + b: { c: 'c' }, + e: brandedString + } +) + +const nestedCombinedReducer = combineReducers({ + a: (state: string = 'a') => state, + b: combineReducers({ + c: (state: string = 'c') => state, + d: (state: string = 'd') => state + }), + e: (state: BrandedString = brandedString) => state +}) + +// @ts-expect-error +const storeWithNestedCombineReducer = createStore(nestedCombinedReducer, { + b: { c: 5 }, + e: brandedString +}) + +const simpleCombinedReducer = combineReducers({ + c: (state: string = 'c') => state, + d: (state: string = 'd') => state +}) + +// @ts-expect-error +const storeWithSimpleCombinedReducer = createStore(simpleCombinedReducer, { + c: 5 +}) + +// Note: It's not necessary that the errors occur on the lines specified, just as long as something errors somewhere +// since the preloaded state doesn't match the reducer type. + +const simpleCombinedReducerWithImplicitState = combineReducers({ + c: (state = 'c') => state, + d: (state = 'd') => state +}) + +// @ts-expect-error +const storeWithSimpleCombinedReducerWithImplicitState = createStore( + simpleCombinedReducerWithImplicitState, + { c: 5 } +) + const storeWithActionReducer = createStore(reducerWithAction) const storeWithActionReducerAndPreloadedState = createStore(reducerWithAction, { a: 'a',