Skip to content

Commit 066fa81

Browse files
cellogtimdorr
authored andcommitted
fix replaceReducer with a store enhancer (#3524)
* fix replaceReducer with a store enhancer * remove erroneous restriction on StateExt * remove the other extension - our store enhancer might add array functionality, for instance * add reasonable defaults for Ext and StateExt * fix state, add a test for non-object-based state * add verification that store extension is also passed to replaceReducer * better fix: set state default based on what base type it is * fix array test * fix typing of StateExt * add mhelmerson example * fix replaceReducer, so that it infers types, fix example test * fix the weird type hacks in the test * add final working example * update based on PR type changes * fix type * update tests to reflect complete examples * merge the changes from index.d.ts into types/store.ts * extend store type * much better approach: only extend the state when we have an extension * fix typing issues not caught before * add link to the place I learned about this
1 parent dbc95c8 commit 066fa81

File tree

7 files changed

+369
-43
lines changed

7 files changed

+369
-43
lines changed

index.d.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -247,15 +247,33 @@ export type Observer<T> = {
247247
next?(value: T): void
248248
}
249249

250+
/**
251+
* Extend the state
252+
*
253+
* This is used by store enhancers and store creators to extend state.
254+
* If there is no state extension, it just returns the state, as is, otherwise
255+
* it returns the state joined with its extension.
256+
*/
257+
export type ExtendState<State, Extension> = [Extension] extends [never]
258+
? State
259+
: State & Extension
260+
250261
/**
251262
* A store is an object that holds the application's state tree.
252263
* There should only be a single store in a Redux app, as the composition
253264
* happens on the reducer level.
254265
*
255266
* @template S The type of state held by this store.
256267
* @template A the type of actions which may be dispatched by this store.
268+
* @template StateExt any extension to state from store enhancers
269+
* @template Ext any extensions to the store from store enhancers
257270
*/
258-
export interface Store<S = any, A extends Action = AnyAction> {
271+
export interface Store<
272+
S = any,
273+
A extends Action = AnyAction,
274+
StateExt = never,
275+
Ext = {}
276+
> {
259277
/**
260278
* Dispatches an action. It is the only way to trigger a state change.
261279
*
@@ -326,9 +344,9 @@ export interface Store<S = any, A extends Action = AnyAction> {
326344
*
327345
* @param nextReducer The reducer for the store to use instead.
328346
*/
329-
replaceReducer<NewState = S, NewActions extends A = A>(
347+
replaceReducer<NewState, NewActions extends Action>(
330348
nextReducer: Reducer<NewState, NewActions>
331-
): Store<NewState, NewActions>
349+
): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext
332350

333351
/**
334352
* Interoperability point for observable/reactive libraries.
@@ -355,15 +373,15 @@ export type DeepPartial<T> = {
355373
* @template StateExt State extension that is mixed into the state type.
356374
*/
357375
export interface StoreCreator {
358-
<S, A extends Action, Ext, StateExt>(
376+
<S, A extends Action, Ext = {}, StateExt = never>(
359377
reducer: Reducer<S, A>,
360378
enhancer?: StoreEnhancer<Ext, StateExt>
361-
): Store<S & StateExt, A> & Ext
362-
<S, A extends Action, Ext, StateExt>(
379+
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
380+
<S, A extends Action, Ext = {}, StateExt = never>(
363381
reducer: Reducer<S, A>,
364382
preloadedState?: PreloadedState<S>,
365383
enhancer?: StoreEnhancer<Ext>
366-
): Store<S & StateExt, A> & Ext
384+
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
367385
}
368386

369387
/**
@@ -417,16 +435,17 @@ export const createStore: StoreCreator
417435
* @template Ext Store extension that is mixed into the Store type.
418436
* @template StateExt State extension that is mixed into the state type.
419437
*/
420-
export type StoreEnhancer<Ext = {}, StateExt = {}> = (
421-
next: StoreEnhancerStoreCreator
438+
export type StoreEnhancer<Ext = {}, StateExt = never> = (
439+
next: StoreEnhancerStoreCreator<Ext, StateExt>
422440
) => StoreEnhancerStoreCreator<Ext, StateExt>
423-
export type StoreEnhancerStoreCreator<Ext = {}, StateExt = {}> = <
441+
442+
export type StoreEnhancerStoreCreator<Ext = {}, StateExt = never> = <
424443
S = any,
425444
A extends Action = AnyAction
426445
>(
427446
reducer: Reducer<S, A>,
428447
preloadedState?: PreloadedState<S>
429-
) => Store<S & StateExt, A> & Ext
448+
) => Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
430449

431450
/* middleware */
432451

src/applyMiddleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export default function applyMiddleware<Ext, S = any>(
5151
): StoreEnhancer<{ dispatch: Ext }>
5252
export default function applyMiddleware(
5353
...middlewares: Middleware[]
54-
): StoreEnhancer {
54+
): StoreEnhancer<any> {
5555
return (createStore: StoreCreator) => <S, A extends AnyAction>(
5656
reducer: Reducer<S, A>,
5757
...args: any[]

src/createStore.ts

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
PreloadedState,
66
StoreEnhancer,
77
Dispatch,
8-
Observer
8+
Observer,
9+
ExtendState
910
} from './types/store'
1011
import { Action } from './types/actions'
1112
import { Reducer } from './types/reducers'
@@ -37,20 +38,35 @@ import isPlainObject from './utils/isPlainObject'
3738
* @returns {Store} A Redux store that lets you read the state, dispatch actions
3839
* and subscribe to changes.
3940
*/
40-
export default function createStore<S, A extends Action, Ext, StateExt>(
41+
export default function createStore<
42+
S,
43+
A extends Action,
44+
Ext = {},
45+
StateExt = never
46+
>(
4147
reducer: Reducer<S, A>,
4248
enhancer?: StoreEnhancer<Ext, StateExt>
43-
): Store<S & StateExt, A> & Ext
44-
export default function createStore<S, A extends Action, Ext, StateExt>(
49+
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
50+
export default function createStore<
51+
S,
52+
A extends Action,
53+
Ext = {},
54+
StateExt = never
55+
>(
4556
reducer: Reducer<S, A>,
4657
preloadedState?: PreloadedState<S>,
47-
enhancer?: StoreEnhancer<Ext>
48-
): Store<S & StateExt, A> & Ext
49-
export default function createStore<S, A extends Action, Ext, StateExt>(
58+
enhancer?: StoreEnhancer<Ext, StateExt>
59+
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
60+
export default function createStore<
61+
S,
62+
A extends Action,
63+
Ext = {},
64+
StateExt = never
65+
>(
5066
reducer: Reducer<S, A>,
5167
preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
52-
enhancer?: StoreEnhancer<Ext>
53-
): Store<S & StateExt, A> & Ext {
68+
enhancer?: StoreEnhancer<Ext, StateExt>
69+
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext {
5470
if (
5571
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
5672
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
@@ -74,7 +90,7 @@ export default function createStore<S, A extends Action, Ext, StateExt>(
7490

7591
return enhancer(createStore)(reducer, preloadedState as PreloadedState<
7692
S
77-
>) as Store<S & StateExt, A> & Ext
93+
>) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
7894
}
7995

8096
if (typeof reducer !== 'function') {
@@ -252,7 +268,7 @@ export default function createStore<S, A extends Action, Ext, StateExt>(
252268
*/
253269
function replaceReducer<NewState, NewActions extends A>(
254270
nextReducer: Reducer<NewState, NewActions>
255-
): Store<NewState, NewActions> {
271+
): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext {
256272
if (typeof nextReducer !== 'function') {
257273
throw new Error('Expected the nextReducer to be a function.')
258274
}
@@ -269,7 +285,13 @@ export default function createStore<S, A extends Action, Ext, StateExt>(
269285
// the new state tree with any relevant data from the old one.
270286
dispatch({ type: ActionTypes.REPLACE } as A)
271287
// change the type of the store by casting it to the new store
272-
return (store as unknown) as Store<NewState, NewActions>
288+
return (store as unknown) as Store<
289+
ExtendState<NewState, StateExt>,
290+
NewActions,
291+
StateExt,
292+
Ext
293+
> &
294+
Ext
273295
}
274296

275297
/**
@@ -317,12 +339,12 @@ export default function createStore<S, A extends Action, Ext, StateExt>(
317339
// the initial state tree.
318340
dispatch({ type: ActionTypes.INIT } as A)
319341

320-
const store: Store<S & StateExt, A> & Ext = ({
342+
const store = ({
321343
dispatch: dispatch as Dispatch<A>,
322344
subscribe,
323345
getState,
324346
replaceReducer,
325347
[$$observable]: observable
326-
} as unknown) as Store<S & StateExt, A> & Ext
348+
} as unknown) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
327349
return store
328350
}

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export {
1919
Store,
2020
StoreCreator,
2121
StoreEnhancer,
22-
StoreEnhancerStoreCreator
22+
StoreEnhancerStoreCreator,
23+
ExtendState
2324
} from './types/store'
2425
// reducers
2526
export {

src/types/store.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22
import { Action, AnyAction } from './actions'
33
import { Reducer } from './reducers'
44

5+
/**
6+
* Extend the state
7+
*
8+
* This is used by store enhancers and store creators to extend state.
9+
* If there is no state extension, it just returns the state, as is, otherwise
10+
* it returns the state joined with its extension.
11+
*
12+
* Reference for future devs:
13+
* https://github.com/microsoft/TypeScript/issues/31751#issuecomment-498526919
14+
*/
15+
export type ExtendState<State, Extension> = [Extension] extends [never]
16+
? State
17+
: State & Extension
18+
519
/**
620
* Internal "virtual" symbol used to make the `CombinedState` type unique.
721
*/
@@ -107,8 +121,15 @@ export type Observer<T> = {
107121
*
108122
* @template S The type of state held by this store.
109123
* @template A the type of actions which may be dispatched by this store.
124+
* @template StateExt any extension to state from store enhancers
125+
* @template Ext any extensions to the store from store enhancers
110126
*/
111-
export interface Store<S = any, A extends Action = AnyAction> {
127+
export interface Store<
128+
S = any,
129+
A extends Action = AnyAction,
130+
StateExt = never,
131+
Ext = {}
132+
> {
112133
/**
113134
* Dispatches an action. It is the only way to trigger a state change.
114135
*
@@ -179,9 +200,9 @@ export interface Store<S = any, A extends Action = AnyAction> {
179200
*
180201
* @param nextReducer The reducer for the store to use instead.
181202
*/
182-
replaceReducer<NewState = S, NewActions extends A = A>(
203+
replaceReducer<NewState, NewActions extends Action>(
183204
nextReducer: Reducer<NewState, NewActions>
184-
): Store<NewState, NewActions>
205+
): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext
185206

186207
/**
187208
* Interoperability point for observable/reactive libraries.
@@ -204,15 +225,15 @@ export interface Store<S = any, A extends Action = AnyAction> {
204225
* @template StateExt State extension that is mixed into the state type.
205226
*/
206227
export interface StoreCreator {
207-
<S, A extends Action, Ext, StateExt>(
228+
<S, A extends Action, Ext = {}, StateExt = never>(
208229
reducer: Reducer<S, A>,
209230
enhancer?: StoreEnhancer<Ext, StateExt>
210-
): Store<S & StateExt, A> & Ext
211-
<S, A extends Action, Ext, StateExt>(
231+
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
232+
<S, A extends Action, Ext = {}, StateExt = never>(
212233
reducer: Reducer<S, A>,
213234
preloadedState?: PreloadedState<S>,
214235
enhancer?: StoreEnhancer<Ext>
215-
): Store<S & StateExt, A> & Ext
236+
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
216237
}
217238

218239
/**
@@ -236,13 +257,13 @@ export interface StoreCreator {
236257
* @template Ext Store extension that is mixed into the Store type.
237258
* @template StateExt State extension that is mixed into the state type.
238259
*/
239-
export type StoreEnhancer<Ext = {}, StateExt = {}> = (
240-
next: StoreEnhancerStoreCreator
260+
export type StoreEnhancer<Ext = {}, StateExt = never> = (
261+
next: StoreEnhancerStoreCreator<Ext, StateExt>
241262
) => StoreEnhancerStoreCreator<Ext, StateExt>
242-
export type StoreEnhancerStoreCreator<Ext = {}, StateExt = {}> = <
263+
export type StoreEnhancerStoreCreator<Ext = {}, StateExt = never> = <
243264
S = any,
244265
A extends Action = AnyAction
245266
>(
246267
reducer: Reducer<S, A>,
247268
preloadedState?: PreloadedState<S>
248-
) => Store<S & StateExt, A> & Ext
269+
) => Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext

0 commit comments

Comments
 (0)