Repository: reduxjs/redux-toolkit
Pull request #304 (2020-01-03 20:40:26)

correctly infer dispatch from provided middlewares
So this is my first attempt at solving #289.

Still a bit rough around the edges, but that's it for today.

- [x] feature itself is working
- [x] type documentation
- [x] skip tests using `as const` in TS 3.3
- [x] update typescript usage documentation

netlify[bot]: Deploy preview for *redux-starter-kit-docs* ready!

Built with commit a321b9263b14b4bb61ad057e3072b130570d6530

https://deploy-preview-304--redux-starter-kit-docs.netlify.com
codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/reduxjs/redux-toolkit/pr/304/builds/7076) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 7b283be2e1191ae18246a15228c18ced2b87a982:

|Sandbox| Source |
|--|--|
|[tender-violet-w7zhs](https://codesandbox.io/s/tender-violet-w7zhs)| Configuration |
|[jovial-davinci-5532y](https://codesandbox.io/s/jovial-davinci-5532y)| Configuration |
|[rsk-github-issues-example](https://codesandbox.io/s/rsk-github-issues-example-jvnx1)| Configuration |

phryneas: Okay, documentation is updated, too. Could you please give this a look @markerikson ? :)
markerikson: Awright, putting this in.

Patch:
diff --git a/docs/usage/usage-with-typescript.md b/docs/usage/usage-with-typescript.md
index f145663b..76682e91 100644
--- a/docs/usage/usage-with-typescript.md
+++ b/docs/usage/usage-with-typescript.md
@@ -42,24 +42,42 @@ const store = configureStore({
 export type AppDispatch = typeof store.dispatch
 ```
 
-### Extending the `Dispatch` type
+### Correct typings for the `Dispatch` type
 
-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:
+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.
 
-```typescript
-const _store = configureStore({
-  /* ... */
-})
+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.
 
-type EnhancedStoreType = {
-  dispatch(action: MyCustomActionType): MyCustomReturnType
-  dispatch(action: MyCustomActionType2): MyCustomReturnType2
-} & typeof _store
+Please note that when calling `getDefaultMiddleware` in TypeScript, you have to provide the state type as a generic argument.
 
-export const store: EnhancedStoreType = _store
-export type AppDispatch = typeof store.dispatch
+```ts
+import { configureStore } from '@reduxjs/toolkit'
+import additionalMiddleware from 'additional-middleware'
+// @ts-ignore
+import untypedMiddleware from 'untyped-middleware'
+import rootReducer from './rootReducer'
+
+type RootState = ReturnType<typeof rootReducer>
+const store = configureStore({
+  reducer: rootReducer,
+  middleware: [
+    // getDefaultMiddleware needs to be called with the state type
+    ...getDefaultMiddleware<RootState>(),
+    // correctly typed middlewares can just be used
+    additionalMiddleware,
+    // you can also manually type middlewares manually
+    untypedMiddleware as Middleware<
+      (action: Action<'specialAction'>) => number,
+      RootState
+    >
+  ] as const // prevent this from becoming just `Array<Middleware>`
+})
+
+type AppDispatch = typeof store.dispatch
 ```
 
+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.
+
 ### Using the extracted `Dispatch` type with React-Redux
 
 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.
diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md
index e70f6598..b3a419e5 100644
--- a/etc/redux-toolkit.api.md
+++ b/etc/redux-toolkit.api.md
@@ -9,6 +9,7 @@ import { AnyAction } from 'redux';
 import { default as createNextState } from 'immer';
 import { createSelector } from 'reselect';
 import { DeepPartial } from 'redux';
+import { Dispatch } from 'redux';
 import { Draft } from 'immer';
 import { EnhancerOptions } from 'redux-devtools-extension';
 import { Middleware } from 'redux';
@@ -17,7 +18,7 @@ import { ReducersMapObject } from 'redux';
 import { Store } from 'redux';
 import { StoreEnhancer } from 'redux';
 import { ThunkAction } from 'redux-thunk';
-import { ThunkDispatch } from 'redux-thunk';
+import { ThunkMiddleware } from 'redux-thunk';
 
 // @public
 export interface ActionCreatorWithNonInferrablePayload<T extends string = string> extends BaseActionCreator<unknown, T> {
@@ -80,13 +81,13 @@ export type CaseReducerWithPrepare<State, Action extends PayloadAction> = {
 export type ConfigureEnhancersCallback = (defaultEnhancers: StoreEnhancer[]) => StoreEnhancer[];
 
 // @public
-export function configureStore<S = any, A extends Action = AnyAction>(options: ConfigureStoreOptions<S, A>): EnhancedStore<S, A>;
+export function configureStore<S = any, A extends Action = AnyAction, M extends Middlewares<S> = [ThunkMiddleware<S>]>(options: ConfigureStoreOptions<S, A, M>): EnhancedStore<S, A, M>;
 
 // @public
-export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
+export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction, M extends Middlewares<S> = Middlewares<S>> {
     devTools?: boolean | EnhancerOptions;
     enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback;
-    middleware?: Middleware<{}, S>[];
+    middleware?: M;
     preloadedState?: DeepPartial<S extends any ? S : S>;
     reducer: Reducer<S, A> | ReducersMapObject<S, A>;
 }
@@ -124,16 +125,19 @@ export interface CreateSliceOptions<State = any, CR extends SliceCaseReducers<St
 export { Draft }
 
 // @public
-export interface EnhancedStore<S = any, A extends Action = AnyAction> extends Store<S, A> {
-    // (undocumented)
-    dispatch: ThunkDispatch<S, any, A>;
+export interface EnhancedStore<S = any, A extends Action = AnyAction, M extends Middlewares<S> = Middlewares<S>> extends Store<S, A> {
+    dispatch: DispatchForMiddlewares<M> & Dispatch<A>;
 }
 
 // @public (undocumented)
 export function findNonSerializableValue(value: unknown, path?: ReadonlyArray<string>, isSerializable?: (value: unknown) => boolean, getEntries?: (value: unknown) => [string, any][]): NonSerializableValue | false;
 
 // @public
-export function getDefaultMiddleware<S = any>(options?: GetDefaultMiddlewareOptions): Middleware<{}, S>[];
+export function getDefaultMiddleware<S = any, O extends Partial<GetDefaultMiddlewareOptions> = {
+    thunk: true;
+    immutableCheck: true;
+    serializableCheck: true;
+}>(options?: O): Array<Middleware<{}, S> | ThunkMiddlewareFor<S, O>>;
 
 // @public
 export function getType<T extends string>(actionCreator: PayloadActionCreator<any, T>): T;
diff --git a/src/configureStore.ts b/src/configureStore.ts
index 560159e9..8a4468d4 100644
--- a/src/configureStore.ts
+++ b/src/configureStore.ts
@@ -10,16 +10,18 @@ import {
   AnyAction,
   StoreEnhancer,
   Store,
-  DeepPartial
+  DeepPartial,
+  Dispatch
 } from 'redux'
 import {
   composeWithDevTools,
   EnhancerOptions as DevToolsOptions
 } from 'redux-devtools-extension'
-import { ThunkDispatch } from 'redux-thunk'
+import { ThunkMiddleware } from 'redux-thunk'
 
 import isPlainObject from './isPlainObject'
 import { getDefaultMiddleware } from './getDefaultMiddleware'
+import { DispatchForMiddlewares } from './tsHelpers'
 
 const IS_PRODUCTION = process.env.NODE_ENV === 'production'
 
@@ -37,7 +39,11 @@ export type ConfigureEnhancersCallback = (
  *
  * @public
  */
-export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
+export interface ConfigureStoreOptions<
+  S = any,
+  A extends Action = AnyAction,
+  M extends Middlewares<S> = Middlewares<S>
+> {
   /**
    * A single reducer function that will be used as the root reducer, or an
    * object of slice reducers that will be passed to `combineReducers()`.
@@ -48,7 +54,7 @@ export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
    * An array of Redux middleware to install. If not supplied, defaults to
    * the set of middleware returned by `getDefaultMiddleware()`.
    */
-  middleware?: Middleware<{}, S>[]
+  middleware?: M
 
   /**
    * Whether to enable Redux DevTools integration. Defaults to `true`.
@@ -82,18 +88,25 @@ export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
   enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback
 }
 
+type Middlewares<S> = ReadonlyArray<Middleware<{}, S>>
+
 /**
  * A Redux store returned by `configureStore()`. Supports dispatching
  * side-effectful _thunks_ in addition to plain actions.
  *
  * @public
  */
-export interface EnhancedStore<S = any, A extends Action = AnyAction>
-  extends Store<S, A> {
+export interface EnhancedStore<
+  S = any,
+  A extends Action = AnyAction,
+  M extends Middlewares<S> = Middlewares<S>
+> extends Store<S, A> {
   /**
+   * The `dispatch` method of your store, enhanced by all it's middlewares.
+   *
    * @inheritdoc
    */
-  dispatch: ThunkDispatch<S, any, A>
+  dispatch: DispatchForMiddlewares<M> & Dispatch<A>
 }
 
 /**
@@ -104,9 +117,11 @@ export interface EnhancedStore<S = any, A extends Action = AnyAction>
  *
  * @public
  */
-export function configureStore<S = any, A extends Action = AnyAction>(
-  options: ConfigureStoreOptions<S, A>
-): EnhancedStore<S, A> {
+export function configureStore<
+  S = any,
+  A extends Action = AnyAction,
+  M extends Middlewares<S> = [ThunkMiddleware<S>]
+>(options: ConfigureStoreOptions<S, A, M>): EnhancedStore<S, A, M> {
   const {
     reducer = undefined,
     middleware = getDefaultMiddleware(),
@@ -147,7 +162,7 @@ export function configureStore<S = any, A extends Action = AnyAction>(
     storeEnhancers = enhancers(storeEnhancers)
   }
 
-  const composedEnhancer = finalCompose(...storeEnhancers) as StoreEnhancer
+  const composedEnhancer = finalCompose(...storeEnhancers) as any
 
   return createStore(
     rootReducer,
diff --git a/src/getDefaultMiddleware.ts b/src/getDefaultMiddleware.ts
index 9bbaa146..ab395818 100644
--- a/src/getDefaultMiddleware.ts
+++ b/src/getDefaultMiddleware.ts
@@ -1,5 +1,5 @@
-import { Middleware } from 'redux'
-import thunkMiddleware from 'redux-thunk'
+import { Middleware, AnyAction } from 'redux'
+import thunkMiddleware, { ThunkMiddleware } from 'redux-thunk'
 /* PROD_START_REMOVE_UMD */
 import createImmutableStateInvariantMiddleware from 'redux-immutable-state-invariant'
 /* PROD_STOP_REMOVE_UMD */
@@ -28,6 +28,14 @@ interface GetDefaultMiddlewareOptions {
   serializableCheck?: boolean | SerializableStateInvariantMiddlewareOptions
 }
 
+type ThunkMiddlewareFor<S, O extends GetDefaultMiddlewareOptions> = O extends {
+  thunk: false
+}
+  ? never
+  : O extends { thunk: { extraArgument: infer E } }
+  ? ThunkMiddleware<S, AnyAction, E>
+  : ThunkMiddleware<S>
+
 /**
  * Returns any array containing the default middleware installed by
  * `configureStore()`. Useful if you want to configure your store with a custom
@@ -37,9 +45,14 @@ interface GetDefaultMiddlewareOptions {
  *
  * @public
  */
-export function getDefaultMiddleware<S = any>(
-  options: GetDefaultMiddlewareOptions = {}
-): Middleware<{}, S>[] {
+export function getDefaultMiddleware<
+  S = any,
+  O extends Partial<GetDefaultMiddlewareOptions> = {
+    thunk: true
+    immutableCheck: true
+    serializableCheck: true
+  }
+>(options: O = {} as O): Array<Middleware<{}, S> | ThunkMiddlewareFor<S, O>> {
   const {
     thunk = true,
     immutableCheck = true,
@@ -86,5 +99,5 @@ export function getDefaultMiddleware<S = any>(
     }
   }
 
-  return middlewareArray
+  return middlewareArray as any
 }
diff --git a/src/tsHelpers.ts b/src/tsHelpers.ts
index c31c5b07..bbde02b8 100644
--- a/src/tsHelpers.ts
+++ b/src/tsHelpers.ts
@@ -1,3 +1,5 @@
+import { Middleware } from 'redux'
+
 /**
  * return True if T is `any`, otherwise return False
  * taken from https://github.com/joonhocho/tsdef
@@ -60,3 +62,28 @@ export type IsUnknownOrNonInferrable<T, True, False> = AtLeastTS35<
   IsUnknown<T, True, False>,
   IsEmptyObj<T, True, IsUnknown<T, True, False>>
 >
+
+/**
+ * Combines all dispatch signatures of all middlewares in the array `M` into
+ * one intersected dispatch signature.
+ */
+export type DispatchForMiddlewares<M> = M extends ReadonlyArray<any>
+  ? UnionToIntersection<
+      M[number] extends infer MiddlewareValues
+        ? MiddlewareValues extends Middleware<infer DispatchExt, any, any>
+          ? DispatchExt extends Function
+            ? DispatchExt
+            : never
+          : never
+        : never
+    >
+  : never
+
+/**
+ * Convert a Union type `(A|B)` to and intersecion type `(A&B)`
+ */
+type UnionToIntersection<U> = (U extends any
+  ? (k: U) => void
+  : never) extends ((k: infer I) => void)
+  ? I
+  : never


Test patch:
diff --git a/src/getDefaultMiddleware.test.ts b/src/getDefaultMiddleware.test.ts
index d56e9156..6791d191 100644
--- a/src/getDefaultMiddleware.test.ts
+++ b/src/getDefaultMiddleware.test.ts
@@ -1,4 +1,4 @@
-import { createStore, applyMiddleware, AnyAction } from 'redux'
+import { AnyAction } from 'redux'
 import { getDefaultMiddleware } from './getDefaultMiddleware'
 import { configureStore } from './configureStore'
 import thunk, { ThunkAction } from 'redux-thunk'
diff --git a/type-tests/files/configureStore.typetest.ts b/type-tests/files/configureStore.typetest.ts
index 4635019d..95509a36 100644
--- a/type-tests/files/configureStore.typetest.ts
+++ b/type-tests/files/configureStore.typetest.ts
@@ -6,7 +6,8 @@ import {
   Reducer,
   Store
 } from 'redux'
-import { configureStore, PayloadAction } from '../../src'
+import { configureStore, PayloadAction, getDefaultMiddleware } from 'src'
+import thunk, { ThunkMiddleware, ThunkAction, ThunkDispatch } from 'redux-thunk'
 
 /*
  * Test: configureStore() requires a valid reducer or reducer map.
@@ -162,20 +163,95 @@ import { configureStore, PayloadAction } from '../../src'
 }
 
 /**
- * Test: Returned store allows dispatching thunks.
+ * Test: Dispatch typings
  */
 {
-  const store = configureStore({
-    reducer: () => 0
-  })
+  type StateA = number
+  const reducerA = () => 0
+  function thunkA() {
+    return ((() => {}) as any) as ThunkAction<Promise<'A'>, StateA, any, any>
+  }
 
-  function incrementMaybe() {
-    return (dispatch: Dispatch) => {
-      if (Math.random() > 0.5) {
-        dispatch({ type: 'increment' })
-      }
-    }
+  type StateB = string
+  function thunkB() {
+    return (dispatch: Dispatch, getState: () => StateB) => {}
   }
+  /**
+   * Test: by default, dispatching Thunks is possible
+   */
+  {
+    const store = configureStore({
+      reducer: reducerA
+    })
 
-  store.dispatch(incrementMaybe())
+    store.dispatch(thunkA())
+    // typings:expect-error
+    store.dispatch(thunkB())
+  }
+  /**
+   * Test: removing the Thunk Middleware
+   */
+  {
+    const store = configureStore({
+      reducer: reducerA,
+      middleware: []
+    })
+    // typings:expect-error
+    store.dispatch(thunkA())
+    // typings:expect-error
+    store.dispatch(thunkB())
+  }
+  /**
+   * Test: adding the thunk middleware by hand
+   */
+  {
+    const store = configureStore({
+      reducer: reducerA,
+      middleware: [thunk] as [ThunkMiddleware<StateA>]
+    })
+    store.dispatch(thunkA())
+    // typings:expect-error
+    store.dispatch(thunkB())
+  }
+  /**
+   * Test: using getDefaultMiddleware
+   */
+  {
+    const store = configureStore({
+      reducer: reducerA,
+      middleware: getDefaultMiddleware<StateA>()
+    })
+
+    store.dispatch(thunkA())
+    // typings:expect-error
+    store.dispatch(thunkB())
+  }
+  /**
+   * Test: custom middleware
+   */
+  {
+    const store = configureStore({
+      reducer: reducerA,
+      middleware: ([] as any) as [Middleware<(a: StateA) => boolean, StateA>]
+    })
+    const result: boolean = store.dispatch(5)
+    // typings:expect-error
+    const result2: string = store.dispatch(5)
+  }
+  /**
+   * Test: multiple custom middleware
+   */
+  {
+    const store = configureStore({
+      reducer: reducerA,
+      middleware: ([] as any) as [
+        Middleware<(a: 'a') => 'A', StateA>,
+        Middleware<(b: 'b') => 'B', StateA>,
+        ThunkMiddleware<StateA>
+      ]
+    })
+    const result: 'A' = store.dispatch('a')
+    const result2: 'B' = store.dispatch('b')
+    const result3: Promise<'A'> = store.dispatch(thunkA())
+  }
 }
diff --git a/type-tests/files/min-3.4/configureStore.typetest.ts b/type-tests/files/min-3.4/configureStore.typetest.ts
new file mode 100644
index 00000000..4a739947
--- /dev/null
+++ b/type-tests/files/min-3.4/configureStore.typetest.ts
@@ -0,0 +1,36 @@
+import { Dispatch, Middleware } from 'redux'
+import { configureStore, getDefaultMiddleware } from 'src'
+import { ThunkAction } from 'redux-thunk'
+
+/**
+ * Test: Dispatch typings
+ */
+{
+  type StateA = number
+  const reducerA = () => 0
+  function thunkA() {
+    return ((() => {}) as any) as ThunkAction<Promise<'A'>, StateA, any, any>
+  }
+
+  type StateB = string
+  function thunkB() {
+    return (dispatch: Dispatch, getState: () => StateB) => {}
+  }
+
+  /**
+   * Test: custom middleware and getDefaultMiddleware
+   */
+  {
+    const store = configureStore({
+      reducer: reducerA,
+      middleware: [
+        ((() => {}) as any) as Middleware<(a: 'a') => 'A', StateA>,
+        ...getDefaultMiddleware<StateA>()
+      ] as const
+    })
+    const result1: 'A' = store.dispatch('a')
+    const result2: Promise<'A'> = store.dispatch(thunkA())
+    // typings:expect-error
+    store.dispatch(thunkB())
+  }
+}
diff --git a/type-tests/lib.ts b/type-tests/lib.ts
new file mode 100644
index 00000000..d8c37341
--- /dev/null
+++ b/type-tests/lib.ts
@@ -0,0 +1,17 @@
+import { check } from 'typings-tester'
+import { versionMajorMinor, sys, findConfigFile } from 'typescript'
+
+export const tsVersion = Number.parseFloat(versionMajorMinor)
+
+export const testIf = (condition: boolean) => (condition ? test : test.skip)
+
+export function checkDirectory(path: string, bail: boolean = false, depth = 1) {
+  const files = sys.readDirectory(path, ['.ts', '.tsx'], [], [], depth)
+  const tsConfigPath = findConfigFile(path, sys.fileExists)
+
+  if (!tsConfigPath) {
+    throw new Error(`Cannot find TypeScript config file in ${path}.`)
+  }
+
+  check(files, tsConfigPath, bail)
+}
diff --git a/type-tests/types-min-3.4.test.ts b/type-tests/types-min-3.4.test.ts
new file mode 100644
index 00000000..b4c04c6b
--- /dev/null
+++ b/type-tests/types-min-3.4.test.ts
@@ -0,0 +1,5 @@
+import { testIf, checkDirectory, tsVersion } from './lib'
+
+testIf(tsVersion >= 3.4)('Types >= 3.4', () => {
+  checkDirectory(`${__dirname}/files/min-3.4`)
+})
diff --git a/type-tests/types.test.ts b/type-tests/types.test.ts
index e959536c..846d683c 100644
--- a/type-tests/types.test.ts
+++ b/type-tests/types.test.ts
@@ -1,4 +1,4 @@
-import { checkDirectory } from 'typings-tester'
+import { checkDirectory } from './lib'
 
 test('Types', () => {
   checkDirectory(`${__dirname}/files`)