Skip to content

Commit 775da05

Browse files
phryneasmarkerikson
andcommitted
correctly infer dispatch from provided middlewares (#304)
* correctly infer dispatch from provided middlewares * simplify getDefaultMiddleware typings * test readability * documentation, update api report * extract tests for >=3.4 * update documentation * Tweak TS usage wording Co-authored-by: Mark Erikson <mark@isquaredsoftware.com>
1 parent 7aea7b0 commit 775da05

11 files changed

+262
-51
lines changed

docs/usage/usage-with-typescript.md

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,42 @@ const store = configureStore({
4242
export type AppDispatch = typeof store.dispatch
4343
```
4444
45-
### Extending the `Dispatch` type
45+
### Correct typings for the `Dispatch` type
4646
47-
By default, this `AppDispatch` type will account only for the already included `redux-thunk` middleware. If you're adding additional middlewares that provide different return types for some actions, you can overload that `AppDispatch` type. While you can just extend the `AppDispatch` type, it's recommended to do so by adding additional type overloads for `dispatch` on the store, to keep everything more consistent:
47+
The type of the `dispatch` function type will be directly inferred from the `middleware` option. So if you add _correctly typed_ middlewares, `dispatch` should already be correctly typed.
4848
49-
```typescript
50-
const _store = configureStore({
51-
/* ... */
52-
})
49+
There might however be cases, where TypeScript decides to simplify your provided middleware array down to just `Array<Middleware>`. In that case, you have to either specify the array type manually as a tuple, or in TS versions >= 3.4, just add `as const` to your definition.
5350
54-
type EnhancedStoreType = {
55-
dispatch(action: MyCustomActionType): MyCustomReturnType
56-
dispatch(action: MyCustomActionType2): MyCustomReturnType2
57-
} & typeof _store
51+
Please note that when calling `getDefaultMiddleware` in TypeScript, you have to provide the state type as a generic argument.
5852
59-
export const store: EnhancedStoreType = _store
60-
export type AppDispatch = typeof store.dispatch
53+
```ts
54+
import { configureStore } from '@reduxjs/toolkit'
55+
import additionalMiddleware from 'additional-middleware'
56+
// @ts-ignore
57+
import untypedMiddleware from 'untyped-middleware'
58+
import rootReducer from './rootReducer'
59+
60+
type RootState = ReturnType<typeof rootReducer>
61+
const store = configureStore({
62+
reducer: rootReducer,
63+
middleware: [
64+
// getDefaultMiddleware needs to be called with the state type
65+
...getDefaultMiddleware<RootState>(),
66+
// correctly typed middlewares can just be used
67+
additionalMiddleware,
68+
// you can also manually type middlewares manually
69+
untypedMiddleware as Middleware<
70+
(action: Action<'specialAction'>) => number,
71+
RootState
72+
>
73+
] as const // prevent this from becoming just `Array<Middleware>`
74+
})
75+
76+
type AppDispatch = typeof store.dispatch
6177
```
6278
79+
If you need any additional reference or examples, [the type tests for `configureStore`](https://github.com/reduxjs/redux-toolkit/blob/master/type-tests/files/configureStore.typetest.ts) contain many different scenarios on how to type this.
80+
6381
### Using the extracted `Dispatch` type with React-Redux
6482
6583
By default, the React-Redux `useDispatch` hook does not contain any types that take middlewares into account. If you need a more specific type for the `dispatch` function when dispatching, you may specify the type of the returned `dispatch` function, or create a custom-typed version of `useSelector`. See [the React-Redux documentation](https://react-redux.js.org/using-react-redux/static-typing#typing-the-usedispatch-hook) for details.

etc/redux-toolkit.api.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { AnyAction } from 'redux';
99
import { default as createNextState } from 'immer';
1010
import { createSelector } from 'reselect';
1111
import { DeepPartial } from 'redux';
12+
import { Dispatch } from 'redux';
1213
import { Draft } from 'immer';
1314
import { EnhancerOptions } from 'redux-devtools-extension';
1415
import { Middleware } from 'redux';
@@ -17,7 +18,7 @@ import { ReducersMapObject } from 'redux';
1718
import { Store } from 'redux';
1819
import { StoreEnhancer } from 'redux';
1920
import { ThunkAction } from 'redux-thunk';
20-
import { ThunkDispatch } from 'redux-thunk';
21+
import { ThunkMiddleware } from 'redux-thunk';
2122

2223
// @public
2324
export interface ActionCreatorWithNonInferrablePayload<T extends string = string> extends BaseActionCreator<unknown, T> {
@@ -80,13 +81,13 @@ export type CaseReducerWithPrepare<State, Action extends PayloadAction> = {
8081
export type ConfigureEnhancersCallback = (defaultEnhancers: StoreEnhancer[]) => StoreEnhancer[];
8182

8283
// @public
83-
export function configureStore<S = any, A extends Action = AnyAction>(options: ConfigureStoreOptions<S, A>): EnhancedStore<S, A>;
84+
export function configureStore<S = any, A extends Action = AnyAction, M extends Middlewares<S> = [ThunkMiddleware<S>]>(options: ConfigureStoreOptions<S, A, M>): EnhancedStore<S, A, M>;
8485

8586
// @public
86-
export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
87+
export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction, M extends Middlewares<S> = Middlewares<S>> {
8788
devTools?: boolean | EnhancerOptions;
8889
enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback;
89-
middleware?: Middleware<{}, S>[];
90+
middleware?: M;
9091
preloadedState?: DeepPartial<S extends any ? S : S>;
9192
reducer: Reducer<S, A> | ReducersMapObject<S, A>;
9293
}
@@ -124,16 +125,19 @@ export interface CreateSliceOptions<State = any, CR extends SliceCaseReducers<St
124125
export { Draft }
125126

126127
// @public
127-
export interface EnhancedStore<S = any, A extends Action = AnyAction> extends Store<S, A> {
128-
// (undocumented)
129-
dispatch: ThunkDispatch<S, any, A>;
128+
export interface EnhancedStore<S = any, A extends Action = AnyAction, M extends Middlewares<S> = Middlewares<S>> extends Store<S, A> {
129+
dispatch: DispatchForMiddlewares<M> & Dispatch<A>;
130130
}
131131

132132
// @public (undocumented)
133133
export function findNonSerializableValue(value: unknown, path?: ReadonlyArray<string>, isSerializable?: (value: unknown) => boolean, getEntries?: (value: unknown) => [string, any][]): NonSerializableValue | false;
134134

135135
// @public
136-
export function getDefaultMiddleware<S = any>(options?: GetDefaultMiddlewareOptions): Middleware<{}, S>[];
136+
export function getDefaultMiddleware<S = any, O extends Partial<GetDefaultMiddlewareOptions> = {
137+
thunk: true;
138+
immutableCheck: true;
139+
serializableCheck: true;
140+
}>(options?: O): Array<Middleware<{}, S> | ThunkMiddlewareFor<S, O>>;
137141

138142
// @public
139143
export function getType<T extends string>(actionCreator: PayloadActionCreator<any, T>): T;

src/configureStore.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ import {
1010
AnyAction,
1111
StoreEnhancer,
1212
Store,
13-
DeepPartial
13+
DeepPartial,
14+
Dispatch
1415
} from 'redux'
1516
import {
1617
composeWithDevTools,
1718
EnhancerOptions as DevToolsOptions
1819
} from 'redux-devtools-extension'
19-
import { ThunkDispatch } from 'redux-thunk'
20+
import { ThunkMiddleware } from 'redux-thunk'
2021

2122
import isPlainObject from './isPlainObject'
2223
import { getDefaultMiddleware } from './getDefaultMiddleware'
24+
import { DispatchForMiddlewares } from './tsHelpers'
2325

2426
const IS_PRODUCTION = process.env.NODE_ENV === 'production'
2527

@@ -37,7 +39,11 @@ export type ConfigureEnhancersCallback = (
3739
*
3840
* @public
3941
*/
40-
export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
42+
export interface ConfigureStoreOptions<
43+
S = any,
44+
A extends Action = AnyAction,
45+
M extends Middlewares<S> = Middlewares<S>
46+
> {
4147
/**
4248
* A single reducer function that will be used as the root reducer, or an
4349
* object of slice reducers that will be passed to `combineReducers()`.
@@ -48,7 +54,7 @@ export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
4854
* An array of Redux middleware to install. If not supplied, defaults to
4955
* the set of middleware returned by `getDefaultMiddleware()`.
5056
*/
51-
middleware?: Middleware<{}, S>[]
57+
middleware?: M
5258

5359
/**
5460
* Whether to enable Redux DevTools integration. Defaults to `true`.
@@ -82,18 +88,25 @@ export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
8288
enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback
8389
}
8490

91+
type Middlewares<S> = ReadonlyArray<Middleware<{}, S>>
92+
8593
/**
8694
* A Redux store returned by `configureStore()`. Supports dispatching
8795
* side-effectful _thunks_ in addition to plain actions.
8896
*
8997
* @public
9098
*/
91-
export interface EnhancedStore<S = any, A extends Action = AnyAction>
92-
extends Store<S, A> {
99+
export interface EnhancedStore<
100+
S = any,
101+
A extends Action = AnyAction,
102+
M extends Middlewares<S> = Middlewares<S>
103+
> extends Store<S, A> {
93104
/**
105+
* The `dispatch` method of your store, enhanced by all it's middlewares.
106+
*
94107
* @inheritdoc
95108
*/
96-
dispatch: ThunkDispatch<S, any, A>
109+
dispatch: DispatchForMiddlewares<M> & Dispatch<A>
97110
}
98111

99112
/**
@@ -104,9 +117,11 @@ export interface EnhancedStore<S = any, A extends Action = AnyAction>
104117
*
105118
* @public
106119
*/
107-
export function configureStore<S = any, A extends Action = AnyAction>(
108-
options: ConfigureStoreOptions<S, A>
109-
): EnhancedStore<S, A> {
120+
export function configureStore<
121+
S = any,
122+
A extends Action = AnyAction,
123+
M extends Middlewares<S> = [ThunkMiddleware<S>]
124+
>(options: ConfigureStoreOptions<S, A, M>): EnhancedStore<S, A, M> {
110125
const {
111126
reducer = undefined,
112127
middleware = getDefaultMiddleware(),
@@ -147,7 +162,7 @@ export function configureStore<S = any, A extends Action = AnyAction>(
147162
storeEnhancers = enhancers(storeEnhancers)
148163
}
149164

150-
const composedEnhancer = finalCompose(...storeEnhancers) as StoreEnhancer
165+
const composedEnhancer = finalCompose(...storeEnhancers) as any
151166

152167
return createStore(
153168
rootReducer,

src/getDefaultMiddleware.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createStore, applyMiddleware, AnyAction } from 'redux'
1+
import { AnyAction } from 'redux'
22
import { getDefaultMiddleware } from './getDefaultMiddleware'
33
import { configureStore } from './configureStore'
44
import thunk, { ThunkAction } from 'redux-thunk'

src/getDefaultMiddleware.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Middleware } from 'redux'
2-
import thunkMiddleware from 'redux-thunk'
1+
import { Middleware, AnyAction } from 'redux'
2+
import thunkMiddleware, { ThunkMiddleware } from 'redux-thunk'
33
/* PROD_START_REMOVE_UMD */
44
import createImmutableStateInvariantMiddleware from 'redux-immutable-state-invariant'
55
/* PROD_STOP_REMOVE_UMD */
@@ -28,6 +28,14 @@ interface GetDefaultMiddlewareOptions {
2828
serializableCheck?: boolean | SerializableStateInvariantMiddlewareOptions
2929
}
3030

31+
type ThunkMiddlewareFor<S, O extends GetDefaultMiddlewareOptions> = O extends {
32+
thunk: false
33+
}
34+
? never
35+
: O extends { thunk: { extraArgument: infer E } }
36+
? ThunkMiddleware<S, AnyAction, E>
37+
: ThunkMiddleware<S>
38+
3139
/**
3240
* Returns any array containing the default middleware installed by
3341
* `configureStore()`. Useful if you want to configure your store with a custom
@@ -37,9 +45,14 @@ interface GetDefaultMiddlewareOptions {
3745
*
3846
* @public
3947
*/
40-
export function getDefaultMiddleware<S = any>(
41-
options: GetDefaultMiddlewareOptions = {}
42-
): Middleware<{}, S>[] {
48+
export function getDefaultMiddleware<
49+
S = any,
50+
O extends Partial<GetDefaultMiddlewareOptions> = {
51+
thunk: true
52+
immutableCheck: true
53+
serializableCheck: true
54+
}
55+
>(options: O = {} as O): Array<Middleware<{}, S> | ThunkMiddlewareFor<S, O>> {
4356
const {
4457
thunk = true,
4558
immutableCheck = true,
@@ -86,5 +99,5 @@ export function getDefaultMiddleware<S = any>(
8699
}
87100
}
88101

89-
return middlewareArray
102+
return middlewareArray as any
90103
}

src/tsHelpers.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Middleware } from 'redux'
2+
13
/**
24
* return True if T is `any`, otherwise return False
35
* taken from https://github.com/joonhocho/tsdef
@@ -60,3 +62,28 @@ export type IsUnknownOrNonInferrable<T, True, False> = AtLeastTS35<
6062
IsUnknown<T, True, False>,
6163
IsEmptyObj<T, True, IsUnknown<T, True, False>>
6264
>
65+
66+
/**
67+
* Combines all dispatch signatures of all middlewares in the array `M` into
68+
* one intersected dispatch signature.
69+
*/
70+
export type DispatchForMiddlewares<M> = M extends ReadonlyArray<any>
71+
? UnionToIntersection<
72+
M[number] extends infer MiddlewareValues
73+
? MiddlewareValues extends Middleware<infer DispatchExt, any, any>
74+
? DispatchExt extends Function
75+
? DispatchExt
76+
: never
77+
: never
78+
: never
79+
>
80+
: never
81+
82+
/**
83+
* Convert a Union type `(A|B)` to and intersecion type `(A&B)`
84+
*/
85+
type UnionToIntersection<U> = (U extends any
86+
? (k: U) => void
87+
: never) extends ((k: infer I) => void)
88+
? I
89+
: never

0 commit comments

Comments
 (0)