diff --git a/src/createStore.ts b/src/createStore.ts index 2560ad863e..9e13aa2291 100644 --- a/src/createStore.ts +++ b/src/createStore.ts @@ -6,7 +6,6 @@ import { StoreEnhancer, Dispatch, Observer, - ExtendState, ListenerCallback } from './types/store' import { Action } from './types/actions' @@ -43,7 +42,7 @@ import { kindOf } from './utils/kindOf' export function createStore( reducer: Reducer, enhancer?: StoreEnhancer -): Store, A, StateExt, Ext> & Ext +): Store & Ext /** * @deprecated * @@ -73,12 +72,12 @@ export function createStore( reducer: Reducer, preloadedState?: PreloadedState, enhancer?: StoreEnhancer -): Store, A, StateExt, Ext> & Ext +): Store & Ext export function createStore( reducer: Reducer, preloadedState?: PreloadedState | StoreEnhancer, enhancer?: StoreEnhancer -): Store, A, StateExt, Ext> & Ext { +): Store & Ext { if (typeof reducer !== 'function') { throw new Error( `Expected the root reducer to be a function. Instead, received: '${kindOf( @@ -115,7 +114,7 @@ export function createStore( return enhancer(createStore)( reducer, preloadedState as PreloadedState - ) as Store, A, StateExt, Ext> & Ext + ) as Store & Ext } let currentReducer = reducer @@ -291,11 +290,8 @@ export function createStore( * implement a hot reloading mechanism for Redux. * * @param nextReducer The reducer for the store to use instead. - * @returns The same store instance with a new reducer in place. */ - function replaceReducer( - nextReducer: Reducer - ): Store, NewActions, StateExt, Ext> & Ext { + function replaceReducer(nextReducer: Reducer): void { if (typeof nextReducer !== 'function') { throw new Error( `Expected the nextReducer to be a function. Instead, received: '${kindOf( @@ -304,22 +300,13 @@ export function createStore( ) } - // TODO: do this more elegantly - ;(currentReducer as unknown as Reducer) = nextReducer + currentReducer = nextReducer // This action has a similar effect to ActionTypes.INIT. // Any reducers that existed in both the new and old rootReducer // will receive the previous state. This effectively populates // the new state tree with any relevant data from the old one. dispatch({ type: ActionTypes.REPLACE } as A) - // change the type of the store by casting it to the new store - return store as unknown as Store< - ExtendState, - NewActions, - StateExt, - Ext - > & - Ext } /** @@ -377,7 +364,7 @@ export function createStore( getState, replaceReducer, [$$observable]: observable - } as unknown as Store, A, StateExt, Ext> & Ext + } as unknown as Store & Ext return store } @@ -419,7 +406,7 @@ export function legacy_createStore< >( reducer: Reducer, enhancer?: StoreEnhancer -): Store, A, StateExt, Ext> & Ext +): Store & Ext /** * Creates a Redux store that holds the state tree. * @@ -459,7 +446,7 @@ export function legacy_createStore< reducer: Reducer, preloadedState?: PreloadedState, enhancer?: StoreEnhancer -): Store, A, StateExt, Ext> & Ext +): Store & Ext export function legacy_createStore< S, A extends Action, @@ -469,6 +456,6 @@ export function legacy_createStore< reducer: Reducer, preloadedState?: PreloadedState | StoreEnhancer, enhancer?: StoreEnhancer -): Store, A, StateExt, Ext> & Ext { +): Store & Ext { return createStore(reducer, preloadedState as any, enhancer) } diff --git a/src/types/store.ts b/src/types/store.ts index 488092b2a2..fb5b1a1d5f 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -133,13 +133,11 @@ export type Observer = { * @template S The type of state held by this store. * @template A the type of actions which may be dispatched by this store. * @template StateExt any extension to state from store enhancers - * @template Ext any extensions to the store from store enhancers */ export interface Store< S = any, A extends Action = AnyAction, - StateExt = never, - Ext = {} + StateExt = never > { /** * Dispatches an action. It is the only way to trigger a state change. @@ -174,7 +172,7 @@ export interface Store< * * @returns The current state tree of your application. */ - getState(): S + getState(): ExtendState /** * Adds a change listener. It will be called any time an action is @@ -211,9 +209,7 @@ export interface Store< * * @param nextReducer The reducer for the store to use instead. */ - replaceReducer( - nextReducer: Reducer - ): Store, NewActions, StateExt, Ext> & Ext + replaceReducer(nextReducer: Reducer): void /** * Interoperability point for observable/reactive libraries. @@ -221,7 +217,7 @@ export interface Store< * For more information, see the observable proposal: * https://github.com/tc39/proposal-observable */ - [Symbol.observable](): Observable + [Symbol.observable](): Observable> } /** @@ -239,12 +235,12 @@ export interface StoreCreator { ( reducer: Reducer, enhancer?: StoreEnhancer - ): Store, A, StateExt, Ext> & Ext + ): Store & Ext ( reducer: Reducer, preloadedState?: PreloadedState, enhancer?: StoreEnhancer - ): Store, A, StateExt, Ext> & Ext + ): Store & Ext } /** @@ -277,4 +273,4 @@ export type StoreEnhancerStoreCreator = < >( reducer: Reducer, preloadedState?: PreloadedState -) => Store, A, StateExt, Ext> & Ext +) => Store & Ext diff --git a/test/combineReducers.spec.ts b/test/combineReducers.spec.ts index eeff719eb1..898c3bc215 100644 --- a/test/combineReducers.spec.ts +++ b/test/combineReducers.spec.ts @@ -327,14 +327,16 @@ describe('Utils', () => { const ACTION = { type: 'ACTION' } it('should return an updated state when additional reducers are passed to combineReducers', function () { - const originalCompositeReducer = combineReducers({ foo }) + type State = { foo: {}; bar?: {} } + + const originalCompositeReducer = combineReducers({ foo }) const store = createStore(originalCompositeReducer) store.dispatch(ACTION) const initialState = store.getState() - store.replaceReducer(combineReducers({ foo, bar })) + store.replaceReducer(combineReducers({ foo, bar })) store.dispatch(ACTION) const nextState = store.getState() @@ -342,16 +344,18 @@ describe('Utils', () => { }) it('should return an updated state when reducers passed to combineReducers are changed', function () { + type State = { foo?: {}; bar: {}; baz?: {} } + const baz = (state = {}) => state - const originalCompositeReducer = combineReducers({ foo, bar }) + const originalCompositeReducer = combineReducers({ foo, bar }) const store = createStore(originalCompositeReducer) store.dispatch(ACTION) const initialState = store.getState() - store.replaceReducer(combineReducers({ baz, bar })) + store.replaceReducer(combineReducers({ baz, bar })) store.dispatch(ACTION) const nextState = store.getState() @@ -374,7 +378,9 @@ 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 }) + const originalCompositeReducer = combineReducers<{ foo?: {}; bar: {} }>( + { foo, bar } + ) const store = createStore(originalCompositeReducer) store.dispatch(ACTION) diff --git a/test/createStore.spec.ts b/test/createStore.spec.ts index f8065aeb61..3b82f37b2e 100644 --- a/test/createStore.spec.ts +++ b/test/createStore.spec.ts @@ -824,7 +824,7 @@ describe('createStore', () => { console.error = jest.fn() const store = createStore( - combineReducers({ + combineReducers<{ x?: number; y: { z: number; w?: number } }>({ x: (s = 0, _) => s, y: combineReducers({ z: (s = 0, _) => s, diff --git a/test/replaceReducers.spec.ts b/test/replaceReducers.spec.ts deleted file mode 100644 index ed70106b12..0000000000 --- a/test/replaceReducers.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createStore, combineReducers } from '..' - -describe('replaceReducers test', () => { - it('returns the original store', () => { - const nextReducer = combineReducers({ - foo: (state = 1, _action) => state, - bar: (state = 2, _action) => state - }) - const store = createStore((state, action) => { - if (state === undefined) return { type: 5 } - return action - }) - - const nextStore = store.replaceReducer(nextReducer) - - expect(nextStore).toBe(store) - }) -}) diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts index 0ec1bdcfad..16cb9b3f3c 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -1,4 +1,11 @@ -import { StoreEnhancer, Action, AnyAction, Reducer, createStore } from '../..' +import { + StoreEnhancer, + Action, + AnyAction, + Reducer, + createStore, + Store +} from '../..' interface State { someField: 'string' @@ -131,22 +138,27 @@ function replaceReducerExtender() { return createStore(wrappedReducer, wrappedPreloadedState) } - const store = createStore(reducer, enhancer) + const store = createStore< + { someField?: 'string'; test?: boolean }, + Action, + { method(): string }, + ExtraState + >(reducer, enhancer) const newReducer = ( state: { test: boolean } = { test: true }, _: AnyAction ) => state - const newStore = store.replaceReducer(newReducer) - newStore.getState().test - newStore.getState().extraField + store.replaceReducer(newReducer) + store.getState().test + store.getState().extraField // @ts-expect-error - newStore.getState().wrongField + store.getState().wrongField - const res: string = newStore.method() + const res: string = store.method() // @ts-expect-error - newStore.wrongMethod() + store.wrongMethod() } function mhelmersonExample() { @@ -187,13 +199,8 @@ function mhelmersonExample() { const store = createStore(wrappedReducer, wrappedPreloadedState) return { ...store, - replaceReducer( - nextReducer: ( - state: (NS & ExtraState) | undefined, - action: NA - ) => NS & ExtraState - ) { - const nextWrappedReducer: Reducer = ( + replaceReducer(nextReducer: Reducer) { + const nextWrappedReducer: Reducer = ( state, action ) => { @@ -208,13 +215,17 @@ function mhelmersonExample() { } } - const store = createStore(reducer, enhancer) + const store = createStore< + { someField?: 'string'; test?: boolean }, + Action, + {}, + ExtraState + >(reducer, enhancer) store.replaceReducer(reducer) store.getState().extraField // @ts-expect-error store.getState().wrongField - // @ts-expect-error store.getState().test const newReducer = ( @@ -222,11 +233,11 @@ function mhelmersonExample() { _: AnyAction ) => state - const newStore = store.replaceReducer(newReducer) - newStore.getState().test - newStore.getState().extraField + store.replaceReducer(newReducer) + store.getState().test + store.getState().extraField // @ts-expect-error - newStore.getState().wrongField + store.getState().wrongField } } @@ -235,12 +246,12 @@ function finalHelmersonExample() { foo: string } - function persistReducer( + function persistReducer>( config: any, reducer: Reducer ) { - return (state: (S & ExtraState) | undefined, action: AnyAction) => { - const newState = reducer(state, action as unknown as A) + return (state: (S & ExtraState) | undefined, action: A) => { + const newState = reducer(state, action) return { ...newState, foo: 'hi' @@ -256,10 +267,10 @@ function finalHelmersonExample() { persistConfig: any ): StoreEnhancer<{}, ExtraState> { return createStore => - ( + >( reducer: Reducer, preloadedState?: any - ) => { + ): Store & { persistor: Store } => { const persistedReducer = persistReducer(persistConfig, reducer) const store = createStore(persistedReducer, preloadedState) const persistor = persistStore(store) @@ -267,16 +278,19 @@ function finalHelmersonExample() { return { ...store, replaceReducer: nextReducer => { - return store.replaceReducer( - persistReducer(persistConfig, nextReducer) - ) + store.replaceReducer(persistReducer(persistConfig, nextReducer)) }, persistor } } } - const store = createStore(reducer, createPersistEnhancer('hi')) + const store = createStore< + { someField?: 'string'; test?: boolean }, + Action, + {}, + ExtraState + >(reducer, createPersistEnhancer('hi')) store.getState().foo // @ts-expect-error @@ -287,10 +301,10 @@ function finalHelmersonExample() { _: AnyAction ) => state - const newStore = store.replaceReducer(newReducer) - newStore.getState().test + store.replaceReducer(newReducer) + store.getState().test // @ts-expect-error - newStore.getState().whatever + store.getState().whatever // @ts-expect-error - newStore.getState().wrongField + store.getState().wrongField } diff --git a/test/typescript/replaceReducer.ts b/test/typescript/replaceReducer.ts deleted file mode 100644 index 66d6bca156..0000000000 --- a/test/typescript/replaceReducer.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { combineReducers, createStore } from '../..' - -/** - * verify that replaceReducer maintains strict typing if the new types change - */ -const bar = (state = { value: 'bar' }) => state -const baz = (state = { value: 'baz' }) => state -const ACTION = { - type: 'action' -} - -const originalCompositeReducer = combineReducers({ bar }) -const store = createStore(originalCompositeReducer) -store.dispatch(ACTION) - -const firstState = store.getState() -firstState.bar.value -// @ts-expect-error -firstState.baz.value - -const nextStore = store.replaceReducer(combineReducers({ baz })) // returns -> { baz: { value: 'baz' }} - -const nextState = nextStore.getState() -// @ts-expect-error -nextState.bar.value -nextState.baz.value