Skip to content

Commit ebe6915

Browse files
committed
Revert adding the store as a return type to replaceReducer
1 parent 6294ad2 commit ebe6915

File tree

8 files changed

+193
-125
lines changed

8 files changed

+193
-125
lines changed

src/createStore.ts

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import {
55
PreloadedState,
66
StoreEnhancer,
77
Dispatch,
8-
Observer,
9-
ExtendState
8+
Observer
109
} from './types/store'
1110
import { Action } from './types/actions'
1211
import { Reducer } from './types/reducers'
@@ -47,7 +46,7 @@ export default function createStore<
4746
>(
4847
reducer: Reducer<S, A>,
4948
enhancer?: StoreEnhancer<Ext, StateExt>
50-
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
49+
): Store<S, A, StateExt> & Ext
5150
export default function createStore<
5251
S,
5352
A extends Action,
@@ -57,7 +56,7 @@ export default function createStore<
5756
reducer: Reducer<S, A>,
5857
preloadedState?: PreloadedState<S>,
5958
enhancer?: StoreEnhancer<Ext, StateExt>
60-
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
59+
): Store<S, A, StateExt> & Ext
6160
export default function createStore<
6261
S,
6362
A extends Action,
@@ -67,7 +66,7 @@ export default function createStore<
6766
reducer: Reducer<S, A>,
6867
preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
6968
enhancer?: StoreEnhancer<Ext, StateExt>
70-
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext {
69+
): Store<S, A, StateExt> & Ext {
7170
if (typeof reducer !== 'function') {
7271
throw new Error(
7372
`Expected the root reducer to be a function. Instead, received: '${kindOf(
@@ -104,7 +103,7 @@ export default function createStore<
104103
return enhancer(createStore)(
105104
reducer,
106105
preloadedState as PreloadedState<S>
107-
) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
106+
) as Store<S, A, StateExt> & Ext
108107
}
109108

110109
let currentReducer = reducer
@@ -278,11 +277,8 @@ export default function createStore<
278277
* implement a hot reloading mechanism for Redux.
279278
*
280279
* @param nextReducer The reducer for the store to use instead.
281-
* @returns The same store instance with a new reducer in place.
282280
*/
283-
function replaceReducer<NewState, NewActions extends A>(
284-
nextReducer: Reducer<NewState, NewActions>
285-
): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext {
281+
function replaceReducer(nextReducer: Reducer<S, A>): void {
286282
if (typeof nextReducer !== 'function') {
287283
throw new Error(
288284
`Expected the nextReducer to be a function. Instead, received: '${kindOf(
@@ -291,22 +287,13 @@ export default function createStore<
291287
)
292288
}
293289

294-
// TODO: do this more elegantly
295-
;(currentReducer as unknown as Reducer<NewState, NewActions>) = nextReducer
290+
currentReducer = nextReducer
296291

297292
// This action has a similar effect to ActionTypes.INIT.
298293
// Any reducers that existed in both the new and old rootReducer
299294
// will receive the previous state. This effectively populates
300295
// the new state tree with any relevant data from the old one.
301296
dispatch({ type: ActionTypes.REPLACE } as A)
302-
// change the type of the store by casting it to the new store
303-
return store as unknown as Store<
304-
ExtendState<NewState, StateExt>,
305-
NewActions,
306-
StateExt,
307-
Ext
308-
> &
309-
Ext
310297
}
311298

312299
/**
@@ -364,6 +351,6 @@ export default function createStore<
364351
getState,
365352
replaceReducer,
366353
[$$observable]: observable
367-
} as unknown as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
354+
} as unknown as Store<S, A, StateExt> & Ext
368355
return store
369356
}

src/types/store.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,11 @@ export type Observer<T> = {
131131
* @template S The type of state held by this store.
132132
* @template A the type of actions which may be dispatched by this store.
133133
* @template StateExt any extension to state from store enhancers
134-
* @template Ext any extensions to the store from store enhancers
135134
*/
136135
export interface Store<
137136
S = any,
138137
A extends Action = AnyAction,
139-
StateExt = never,
140-
Ext = {}
138+
StateExt = never
141139
> {
142140
/**
143141
* Dispatches an action. It is the only way to trigger a state change.
@@ -172,7 +170,7 @@ export interface Store<
172170
*
173171
* @returns The current state tree of your application.
174172
*/
175-
getState(): S
173+
getState(): ExtendState<S, StateExt>
176174

177175
/**
178176
* Adds a change listener. It will be called any time an action is
@@ -209,17 +207,15 @@ export interface Store<
209207
*
210208
* @param nextReducer The reducer for the store to use instead.
211209
*/
212-
replaceReducer<NewState, NewActions extends Action>(
213-
nextReducer: Reducer<NewState, NewActions>
214-
): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext
210+
replaceReducer(nextReducer: Reducer<S, A>): void
215211

216212
/**
217213
* Interoperability point for observable/reactive libraries.
218214
* @returns {observable} A minimal observable of state changes.
219215
* For more information, see the observable proposal:
220216
* https://github.com/tc39/proposal-observable
221217
*/
222-
[Symbol.observable](): Observable<S>
218+
[Symbol.observable](): Observable<ExtendState<S, StateExt>>
223219
}
224220

225221
/**
@@ -237,12 +233,12 @@ export interface StoreCreator {
237233
<S, A extends Action, Ext = {}, StateExt = never>(
238234
reducer: Reducer<S, A>,
239235
enhancer?: StoreEnhancer<Ext, StateExt>
240-
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
236+
): Store<S, A, StateExt> & Ext
241237
<S, A extends Action, Ext = {}, StateExt = never>(
242238
reducer: Reducer<S, A>,
243239
preloadedState?: PreloadedState<S>,
244240
enhancer?: StoreEnhancer<Ext>
245-
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
241+
): Store<S, A, StateExt> & Ext
246242
}
247243

248244
/**
@@ -275,4 +271,4 @@ export type StoreEnhancerStoreCreator<Ext = {}, StateExt = never> = <
275271
>(
276272
reducer: Reducer<S, A>,
277273
preloadedState?: PreloadedState<S>
278-
) => Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
274+
) => Store<S, A, StateExt> & Ext

test/combineReducers.spec.ts

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
/* eslint-disable no-console */
22
import {
3-
createStore,
4-
combineReducers,
5-
Reducer,
3+
__DO_NOT_USE__ActionTypes as ActionTypes,
4+
Action,
65
AnyAction,
7-
__DO_NOT_USE__ActionTypes as ActionTypes
6+
combineReducers,
7+
createStore,
8+
Reducer
89
} from '..'
10+
import { combineReducersAccuratelyTyped } from './helpers/combineReducersTypes'
911

1012
describe('Utils', () => {
1113
describe('combineReducers', () => {
@@ -327,61 +329,93 @@ describe('Utils', () => {
327329
const ACTION = { type: 'ACTION' }
328330

329331
it('should return an updated state when additional reducers are passed to combineReducers', function () {
330-
const originalCompositeReducer = combineReducers({ foo })
332+
type Reducers = {
333+
foo: Reducer<{}, Action<unknown>>
334+
bar?: Reducer<{}, Action<unknown>>
335+
}
336+
337+
const originalCompositeReducer =
338+
combineReducersAccuratelyTyped<Reducers>({ foo })
331339
const store = createStore(originalCompositeReducer)
332340

333341
store.dispatch(ACTION)
334342

335343
const initialState = store.getState()
336344

337-
store.replaceReducer(combineReducers({ foo, bar }))
345+
store.replaceReducer(
346+
combineReducersAccuratelyTyped<Reducers>({
347+
foo,
348+
bar
349+
})
350+
)
338351
store.dispatch(ACTION)
339352

340353
const nextState = store.getState()
341354
expect(nextState).not.toBe(initialState)
342355
})
343356

344357
it('should return an updated state when reducers passed to combineReducers are changed', function () {
358+
type Reducers = {
359+
foo?: Reducer<{}, Action<unknown>>
360+
bar: Reducer<{}, Action<unknown>>
361+
baz?: Reducer<{}, Action<unknown>>
362+
}
363+
345364
const baz = (state = {}) => state
346365

347-
const originalCompositeReducer = combineReducers({ foo, bar })
366+
const originalCompositeReducer =
367+
combineReducersAccuratelyTyped<Reducers>({
368+
foo,
369+
bar
370+
})
348371
const store = createStore(originalCompositeReducer)
349372

350373
store.dispatch(ACTION)
351374

352375
const initialState = store.getState()
353376

354-
store.replaceReducer(combineReducers({ baz, bar }))
377+
store.replaceReducer(
378+
combineReducersAccuratelyTyped<Reducers>({ baz, bar })
379+
)
355380
store.dispatch(ACTION)
356381

357382
const nextState = store.getState()
358383
expect(nextState).not.toBe(initialState)
359384
})
360385

361386
it('should return the same state when reducers passed to combineReducers not changed', function () {
362-
const originalCompositeReducer = combineReducers({ foo, bar })
387+
const originalCompositeReducer = combineReducersAccuratelyTyped({
388+
foo,
389+
bar
390+
})
363391
const store = createStore(originalCompositeReducer)
364392

365393
store.dispatch(ACTION)
366394

367395
const initialState = store.getState()
368396

369-
store.replaceReducer(combineReducers({ foo, bar }))
397+
store.replaceReducer(combineReducersAccuratelyTyped({ foo, bar }))
370398
store.dispatch(ACTION)
371399

372400
const nextState = store.getState()
373401
expect(nextState).toBe(initialState)
374402
})
375403

376404
it('should return an updated state when one of more reducers passed to the combineReducers are removed', function () {
377-
const originalCompositeReducer = combineReducers({ foo, bar })
405+
const originalCompositeReducer = combineReducersAccuratelyTyped<{
406+
foo?: Reducer<{}, Action<unknown>>
407+
bar: Reducer<{}, Action<unknown>>
408+
}>({
409+
foo,
410+
bar
411+
})
378412
const store = createStore(originalCompositeReducer)
379413

380414
store.dispatch(ACTION)
381415

382416
const initialState = store.getState()
383417

384-
store.replaceReducer(combineReducers({ bar }))
418+
store.replaceReducer(combineReducersAccuratelyTyped({ bar }))
385419

386420
const nextState = store.getState()
387421
expect(nextState).not.toBe(initialState)

test/createStore.spec.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { createStore, combineReducers, StoreEnhancer, Action, Store } from '..'
1+
import {
2+
createStore,
3+
combineReducers,
4+
StoreEnhancer,
5+
Action,
6+
Store,
7+
Reducer
8+
} from '..'
29
import {
310
addTodo,
411
dispatchInMiddle,
@@ -12,6 +19,10 @@ import * as reducers from './helpers/reducers'
1219
import { from, ObservableInput } from 'rxjs'
1320
import { map } from 'rxjs/operators'
1421
import $$observable from '../src/utils/symbol-observable'
22+
import {
23+
combineReducersAccuratelyTyped,
24+
ReducerThatAllowsForPartialInputState
25+
} from './helpers/combineReducersTypes'
1526

1627
describe('createStore', () => {
1728
it('exposes the public API', () => {
@@ -823,19 +834,30 @@ describe('createStore', () => {
823834
const originalConsoleError = console.error
824835
console.error = jest.fn()
825836

837+
type YState = { z: number; w?: number }
838+
839+
type Reducers = {
840+
x?: Reducer<number, Action<unknown>>
841+
y: ReducerThatAllowsForPartialInputState<
842+
YState,
843+
Action<unknown>,
844+
Partial<YState> | undefined
845+
>
846+
}
847+
826848
const store = createStore(
827-
combineReducers({
849+
combineReducersAccuratelyTyped<Reducers>({
828850
x: (s = 0, _) => s,
829-
y: combineReducers({
851+
y: combineReducersAccuratelyTyped({
830852
z: (s = 0, _) => s,
831853
w: (s = 0, _) => s
832854
})
833855
})
834856
)
835857

836858
store.replaceReducer(
837-
combineReducers({
838-
y: combineReducers({
859+
combineReducersAccuratelyTyped({
860+
y: combineReducersAccuratelyTyped({
839861
z: (s = 0, _) => s
840862
})
841863
})

test/helpers/combineReducersTypes.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {
2+
Action,
3+
ActionFromReducersMapObject,
4+
combineReducers,
5+
Reducer,
6+
ReducersMapObject
7+
} from '../..'
8+
9+
/**
10+
* The existing `Reducer` type assumes that the input state is `S` or `undefined`. This works for most cases, but
11+
* `combineReducers` allows for properties in the input state to be missing. This type allows for that scenario and is
12+
* used by `combineReducersAccuratelyTyped`. It can be removed if the `Reducer` type in the core library is updated to
13+
* allow for this scenario.
14+
*/
15+
export type ReducerThatAllowsForPartialInputState<
16+
S extends InputState,
17+
A extends Action<unknown>,
18+
InputState
19+
> = (state: InputState, action: A) => S
20+
21+
type ReducersMapObjectThatAllowsForPartialInputState<
22+
S extends InputState,
23+
A extends Action<unknown>,
24+
InputState
25+
> = {
26+
[K in keyof InputState]: ReducerThatAllowsForPartialInputState<
27+
S[K],
28+
A,
29+
InputState[K]
30+
>
31+
}
32+
33+
type StateFromReducersMapObjectThatAllowsForPartialInputState<
34+
M extends ReducersMapObjectThatAllowsForPartialInputState<
35+
unknown,
36+
Action<unknown>,
37+
unknown
38+
>
39+
> = {
40+
[P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never
41+
}
42+
43+
/**
44+
* The `combineReducers` function creates a reducer that allows for missing properties in the input state at the
45+
* top-level, but the types do not accurately reflect that. This method provides accurate types for
46+
* `combineReducers` in order to get the tests that use `replaceReducer` to type-check successfully. This can be
47+
* removed if the types for `combineReducers` are fixed.
48+
*/
49+
export function combineReducersAccuratelyTyped<M extends ReducersMapObject>(
50+
reducers: M
51+
): (
52+
state:
53+
| Partial<StateFromReducersMapObjectThatAllowsForPartialInputState<M>>
54+
| undefined,
55+
action: ActionFromReducersMapObject<M>
56+
) => StateFromReducersMapObjectThatAllowsForPartialInputState<M> {
57+
// @ts-ignore
58+
return combineReducers(reducers)
59+
}

0 commit comments

Comments
 (0)