Skip to content

Commit f1afad8

Browse files
authored
Merge pull request #2550 from fostyfost/feature/store-enhancer-generics
2 parents 24dbdf1 + 4b09d16 commit f1afad8

File tree

3 files changed

+94
-38
lines changed

3 files changed

+94
-38
lines changed

packages/toolkit/src/configureStore.ts

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import type {
2020
CurriedGetDefaultMiddleware,
2121
} from './getDefaultMiddleware'
2222
import { curryGetDefaultMiddleware } from './getDefaultMiddleware'
23-
import type { NoInfer, ExtractDispatchExtensions } from './tsHelpers'
23+
import type {
24+
NoInfer,
25+
ExtractDispatchExtensions,
26+
ExtractStoreExtensions,
27+
} from './tsHelpers'
2428

2529
const IS_PRODUCTION = process.env.NODE_ENV === 'production'
2630

@@ -29,9 +33,9 @@ const IS_PRODUCTION = process.env.NODE_ENV === 'production'
2933
*
3034
* @public
3135
*/
32-
export type ConfigureEnhancersCallback = (
33-
defaultEnhancers: readonly StoreEnhancer[]
34-
) => StoreEnhancer[]
36+
export type ConfigureEnhancersCallback<E extends Enhancers = Enhancers> = (
37+
defaultEnhancers: readonly StoreEnhancer[]
38+
) => [...E]
3539

3640
/**
3741
* Options for `configureStore()`.
@@ -41,7 +45,8 @@ export type ConfigureEnhancersCallback = (
4145
export interface ConfigureStoreOptions<
4246
S = any,
4347
A extends Action = AnyAction,
44-
M extends Middlewares<S> = Middlewares<S>
48+
M extends Middlewares<S> = Middlewares<S>,
49+
E extends Enhancers = Enhancers
4550
> {
4651
/**
4752
* A single reducer function that will be used as the root reducer, or an
@@ -52,7 +57,7 @@ export interface ConfigureStoreOptions<
5257
/**
5358
* An array of Redux middleware to install. If not supplied, defaults to
5459
* the set of middleware returned by `getDefaultMiddleware()`.
55-
*
60+
*
5661
* @example `middleware: (gDM) => gDM().concat(logger, apiMiddleware, yourCustomMiddleware)`
5762
* @see https://redux-toolkit.js.org/api/getDefaultMiddleware#intended-usage
5863
*/
@@ -79,7 +84,7 @@ export interface ConfigureStoreOptions<
7984
- if it is not, there could be two cases:
8085
- `ReducersMapObject<S, A>` is being passed in. In this case, we will call `combineReducers` on it and `CombinedState<S>` is correct
8186
- `Reducer<S, A>` is being passed in. In this case, actually `CombinedState<S>` is wrong and `S` would be correct.
82-
As we cannot distinguish between those two cases without adding another generic paramter,
87+
As we cannot distinguish between those two cases without adding another generic parameter,
8388
we just make the pragmatic assumption that the latter almost never happens.
8489
*/
8590
preloadedState?: PreloadedState<CombinedState<NoInfer<S>>>
@@ -92,21 +97,17 @@ export interface ConfigureStoreOptions<
9297
* and should return a new array (such as `[applyMiddleware, offline]`).
9398
* If you only need to add middleware, you can use the `middleware` parameter instead.
9499
*/
95-
enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback
100+
enhancers?: E | ConfigureEnhancersCallback<E>
96101
}
97102

98103
type Middlewares<S> = ReadonlyArray<Middleware<{}, S>>
99104

100-
/**
101-
* A Redux store returned by `configureStore()`. Supports dispatching
102-
* side-effectful _thunks_ in addition to plain actions.
103-
*
104-
* @public
105-
*/
106-
export interface EnhancedStore<
105+
type Enhancers = ReadonlyArray<StoreEnhancer>
106+
107+
interface ToolkitStore<
107108
S = any,
108109
A extends Action = AnyAction,
109-
M extends Middlewares<S> = Middlewares<S>
110+
M extends Middlewares<S> = Middlewares<S>,
110111
> extends Store<S, A> {
111112
/**
112113
* The `dispatch` method of your store, enhanced by all its middlewares.
@@ -116,19 +117,33 @@ export interface EnhancedStore<
116117
dispatch: ExtractDispatchExtensions<M> & Dispatch<A>
117118
}
118119

120+
/**
121+
* A Redux store returned by `configureStore()`. Supports dispatching
122+
* side-effectful _thunks_ in addition to plain actions.
123+
*
124+
* @public
125+
*/
126+
export type EnhancedStore<
127+
S = any,
128+
A extends Action = AnyAction,
129+
M extends Middlewares<S> = Middlewares<S>,
130+
E extends Enhancers = Enhancers
131+
> = ToolkitStore<S, A, M> & ExtractStoreExtensions<E>
132+
119133
/**
120134
* A friendly abstraction over the standard Redux `createStore()` function.
121135
*
122-
* @param config The store configuration.
136+
* @param options The store configuration.
123137
* @returns A configured Redux store.
124138
*
125139
* @public
126140
*/
127141
export function configureStore<
128142
S = any,
129143
A extends Action = AnyAction,
130-
M extends Middlewares<S> = [ThunkMiddlewareFor<S>]
131-
>(options: ConfigureStoreOptions<S, A, M>): EnhancedStore<S, A, M> {
144+
M extends Middlewares<S> = [ThunkMiddlewareFor<S>],
145+
E extends Enhancers = [StoreEnhancer]
146+
>(options: ConfigureStoreOptions<S, A, M, E>): EnhancedStore<S, A, M, E> {
132147
const curriedGetDefaultMiddleware = curryGetDefaultMiddleware<S>()
133148

134149
const {
@@ -170,7 +185,7 @@ export function configureStore<
170185
)
171186
}
172187

173-
const middlewareEnhancer = applyMiddleware(...finalMiddleware)
188+
const middlewareEnhancer: StoreEnhancer = applyMiddleware(...finalMiddleware)
174189

175190
let finalCompose = compose
176191

@@ -182,15 +197,15 @@ export function configureStore<
182197
})
183198
}
184199

185-
let storeEnhancers: StoreEnhancer[] = [middlewareEnhancer]
200+
let storeEnhancers: Enhancers = [middlewareEnhancer]
186201

187202
if (Array.isArray(enhancers)) {
188203
storeEnhancers = [middlewareEnhancer, ...enhancers]
189204
} else if (typeof enhancers === 'function') {
190205
storeEnhancers = enhancers(storeEnhancers)
191206
}
192207

193-
const composedEnhancer = finalCompose(...storeEnhancers) as any
208+
const composedEnhancer = finalCompose(...storeEnhancers) as StoreEnhancer<any>
194209

195210
return createStore(rootReducer, preloadedState, composedEnhancer)
196211
}

packages/toolkit/src/tests/configureStore.typetest.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import type {
66
Reducer,
77
Store,
88
Action,
9+
StoreEnhancer
910
} from 'redux'
1011
import { applyMiddleware } from 'redux'
11-
import type { PayloadAction, MiddlewareArray } from '@reduxjs/toolkit'
12+
import type { PayloadAction } from '@reduxjs/toolkit'
1213
import {
1314
configureStore,
1415
getDefaultMiddleware,
@@ -17,7 +18,6 @@ import {
1718
import type { ThunkMiddleware, ThunkAction, ThunkDispatch } from 'redux-thunk'
1819
import thunk from 'redux-thunk'
1920
import { expectNotAny, expectType } from './helpers'
20-
import type { IsAny, ExtractDispatchExtensions } from '../tsHelpers'
2121

2222
const _anyMiddleware: any = () => () => () => {}
2323

@@ -140,16 +140,53 @@ const _anyMiddleware: any = () => () => () => {}
140140
* Test: configureStore() accepts store enhancer.
141141
*/
142142
{
143-
configureStore({
144-
reducer: () => 0,
145-
enhancers: [applyMiddleware((store) => (next) => next)],
146-
})
143+
{
144+
const store = configureStore({
145+
reducer: () => 0,
146+
enhancers: [applyMiddleware(() => next => next)]
147+
})
148+
149+
expectType<Dispatch & ThunkDispatch<number, undefined, AnyAction>>(store.dispatch)
150+
}
147151

148152
configureStore({
149153
reducer: () => 0,
150154
// @ts-expect-error
151155
enhancers: ['not a store enhancer'],
152156
})
157+
158+
{
159+
type SomePropertyStoreEnhancer = StoreEnhancer<{ someProperty: string }>
160+
161+
const somePropertyStoreEnhancer: SomePropertyStoreEnhancer = next => {
162+
return (reducer, preloadedState) => {
163+
return {
164+
...next(reducer, preloadedState),
165+
someProperty: 'some value',
166+
}
167+
}
168+
}
169+
170+
type AnotherPropertyStoreEnhancer = StoreEnhancer<{ anotherProperty: number }>
171+
172+
const anotherPropertyStoreEnhancer: AnotherPropertyStoreEnhancer = next => {
173+
return (reducer, preloadedState) => {
174+
return {
175+
...next(reducer, preloadedState),
176+
anotherProperty: 123,
177+
}
178+
}
179+
}
180+
181+
const store = configureStore({
182+
reducer: () => 0,
183+
enhancers: [somePropertyStoreEnhancer, anotherPropertyStoreEnhancer],
184+
})
185+
186+
expectType<Dispatch & ThunkDispatch<number, undefined, AnyAction>>(store.dispatch)
187+
expectType<string>(store.someProperty)
188+
expectType<number>(store.anotherProperty)
189+
}
153190
}
154191

155192
/**

packages/toolkit/src/tsHelpers.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Middleware } from 'redux'
1+
import type { Middleware, StoreEnhancer } from 'redux'
22
import type { MiddlewareArray } from './utils'
33

44
/**
@@ -66,6 +66,15 @@ export type IsUnknownOrNonInferrable<T, True, False> = AtLeastTS35<
6666
IsEmptyObj<T, True, IsUnknown<T, True, False>>
6767
>
6868

69+
/**
70+
* Convert a Union type `(A|B)` to an intersection type `(A&B)`
71+
*/
72+
export type UnionToIntersection<U> = (
73+
U extends any ? (k: U) => void : never
74+
) extends (k: infer I) => void
75+
? I
76+
: never
77+
6978
// Appears to have a convenient side effect of ignoring `never` even if that's not what you specified
7079
export type ExcludeFromTuple<T, E, Acc extends unknown[] = []> = T extends [
7180
infer Head,
@@ -80,7 +89,7 @@ type ExtractDispatchFromMiddlewareTuple<
8089
> = MiddlewareTuple extends [infer Head, ...infer Tail]
8190
? ExtractDispatchFromMiddlewareTuple<
8291
Tail,
83-
Acc & (Head extends Middleware<infer D, any> ? IsAny<D, {}, D> : {})
92+
Acc & (Head extends Middleware<infer D> ? IsAny<D, {}, D> : {})
8493
>
8594
: Acc
8695

@@ -92,14 +101,9 @@ export type ExtractDispatchExtensions<M> = M extends MiddlewareArray<
92101
? ExtractDispatchFromMiddlewareTuple<[...M], {}>
93102
: never
94103

95-
/**
96-
* Convert a Union type `(A|B)` to an intersection type `(A&B)`
97-
*/
98-
export type UnionToIntersection<U> = (
99-
U extends any ? (k: U) => void : never
100-
) extends (k: infer I) => void
101-
? I
102-
: never
104+
export type ExtractStoreExtensions<E> = E extends any[]
105+
? UnionToIntersection<E[number] extends StoreEnhancer<infer Ext> ? Ext extends {} ? Ext : {} : {}>
106+
: {}
103107

104108
/**
105109
* Helper type. Passes T out again, but boxes it in a way that it cannot

0 commit comments

Comments
 (0)