Skip to content

Commit 082519d

Browse files
committed
feat: Add the ability to type StoreEnhancers
fix #299 fix #583 fix #699 fix #2241
1 parent 4a333c2 commit 082519d

File tree

3 files changed

+63
-32
lines changed

3 files changed

+63
-32
lines changed

packages/toolkit/src/configureStore.ts

Lines changed: 28 additions & 21 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,7 @@ 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> = (defaultEnhancers: E) => E
3537

3638
/**
3739
* Options for `configureStore()`.
@@ -41,7 +43,8 @@ export type ConfigureEnhancersCallback = (
4143
export interface ConfigureStoreOptions<
4244
S = any,
4345
A extends Action = AnyAction,
44-
M extends Middlewares<S> = Middlewares<S>
46+
M extends Middlewares<S> = Middlewares<S>,
47+
E extends Enhancers = Enhancers
4548
> {
4649
/**
4750
* A single reducer function that will be used as the root reducer, or an
@@ -52,7 +55,7 @@ export interface ConfigureStoreOptions<
5255
/**
5356
* An array of Redux middleware to install. If not supplied, defaults to
5457
* the set of middleware returned by `getDefaultMiddleware()`.
55-
*
58+
*
5659
* @example `middleware: (gDM) => gDM().concat(logger, apiMiddleware, yourCustomMiddleware)`
5760
* @see https://redux-toolkit.js.org/api/getDefaultMiddleware#intended-usage
5861
*/
@@ -92,43 +95,47 @@ export interface ConfigureStoreOptions<
9295
* and should return a new array (such as `[applyMiddleware, offline]`).
9396
* If you only need to add middleware, you can use the `middleware` parameter instead.
9497
*/
95-
enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback
98+
enhancers?: E | ConfigureEnhancersCallback<E>
9699
}
97100

98101
type Middlewares<S> = ReadonlyArray<Middleware<{}, S>>
99102

103+
type Enhancers = ReadonlyArray<StoreEnhancer>
104+
100105
/**
101106
* A Redux store returned by `configureStore()`. Supports dispatching
102107
* side-effectful _thunks_ in addition to plain actions.
103108
*
104109
* @public
105110
*/
106-
export interface EnhancedStore<
111+
export type EnhancedStore<
107112
S = any,
108113
A extends Action = AnyAction,
109-
M extends Middlewares<S> = Middlewares<S>
110-
> extends Store<S, A> {
111-
/**
112-
* The `dispatch` method of your store, enhanced by all its middlewares.
113-
*
114-
* @inheritdoc
115-
*/
116-
dispatch: ExtractDispatchExtensions<M> & Dispatch<A>
117-
}
114+
M extends Middlewares<S> = Middlewares<S>,
115+
E extends Enhancers = Enhancers
116+
> = Store<S, A> & {
117+
/**
118+
* The `dispatch` method of your store, enhanced by all its middlewares.
119+
*
120+
* @inheritdoc
121+
*/
122+
dispatch: ExtractDispatchExtensions<M> & Dispatch<A>
123+
} & ExtractStoreExtensions<E>
118124

119125
/**
120126
* A friendly abstraction over the standard Redux `createStore()` function.
121127
*
122-
* @param config The store configuration.
128+
* @param options The store configuration.
123129
* @returns A configured Redux store.
124130
*
125131
* @public
126132
*/
127133
export function configureStore<
128134
S = any,
129135
A extends Action = AnyAction,
130-
M extends Middlewares<S> = [ThunkMiddlewareFor<S>]
131-
>(options: ConfigureStoreOptions<S, A, M>): EnhancedStore<S, A, M> {
136+
M extends Middlewares<S> = [ThunkMiddlewareFor<S>],
137+
E extends Enhancers = []
138+
>(options: ConfigureStoreOptions<S, A, M>): EnhancedStore<S, A, M, E> {
132139
const curriedGetDefaultMiddleware = curryGetDefaultMiddleware<S>()
133140

134141
const {
@@ -182,7 +189,7 @@ export function configureStore<
182189
})
183190
}
184191

185-
let storeEnhancers: StoreEnhancer[] = [middlewareEnhancer]
192+
let storeEnhancers: ReadonlyArray<StoreEnhancer> = [middlewareEnhancer]
186193

187194
if (Array.isArray(enhancers)) {
188195
storeEnhancers = [middlewareEnhancer, ...enhancers]

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ 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'
13+
import type { ThunkMiddlewareFor } from '@reduxjs/toolkit'
1214
import {
1315
configureStore,
1416
getDefaultMiddleware,
@@ -150,6 +152,24 @@ const _anyMiddleware: any = () => () => () => {}
150152
// @ts-expect-error
151153
enhancers: ['not a store enhancer'],
152154
})
155+
156+
type SomePropertyStoreEnhancer = StoreEnhancer<{ someProperty: string }>
157+
158+
const somePropertyStoreEnhancer: SomePropertyStoreEnhancer = next => {
159+
return (reducer, preloadedState) => {
160+
return {
161+
...next(reducer, preloadedState),
162+
someProperty: 'some value',
163+
}
164+
}
165+
}
166+
167+
const store = configureStore<any, AnyAction, [ThunkMiddlewareFor<any>], [SomePropertyStoreEnhancer]>({
168+
reducer: () => 0,
169+
enhancers: [somePropertyStoreEnhancer],
170+
})
171+
172+
const someProperty: string = store.someProperty
153173
}
154174

155175
/**

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<
105+
E extends ReadonlyArray<StoreEnhancer>
106+
> = UnionToIntersection<E[number] extends StoreEnhancer<infer Ext> ? Ext : {}>
103107

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

0 commit comments

Comments
 (0)