Repository: reduxjs/redux-toolkit
Pull request #364 (2020-02-16 11:27:12)

use ThunkApiConfig for optional type arguments
I think this is what @Ethan-Arrowood suggested in [this tweet](https://twitter.com/ArrowoodTech/status/1228908646551183360).

It actually is quite a different pattern from what we've been using, but also quite interesting.

@markerikson, @Ethan-Arrowood could you take a look?

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

Built with commit f5e32045278564402963081ea4e354577543b233

https://deploy-preview-364--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/364/builds/11154) or the icon next to each commit SHA.
phryneas: https://github.com/reduxjs/redux-toolkit/blob/f5e32045278564402963081ea4e354577543b233/type-tests/files/createAsyncThunk.typetest.ts#L78-L85

This is essentially how it would be used. I've left the `ReturnValue` and `ThunkArgs` parameters as-is, as these should be automatically inferred in simple use cases. The third parameter takes optional properties and other properties fall back to sane defaults. The `dispatch` fallback in this case even references the user-provided `state` and `extra` types.

Patch:
diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md
index ff219861..669a6342 100644
--- a/etc/redux-toolkit.api.md
+++ b/etc/redux-toolkit.api.md
@@ -18,6 +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
@@ -102,7 +103,7 @@ export function createAction<P = void, T extends string = string>(type: T): Payl
 export function createAction<PA extends PrepareAction<any>, T extends string = string>(type: T, prepareAction: PA): PayloadActionCreator<ReturnType<PA>['payload'], T, PA>;
 
 // @alpha (undocumented)
-export function createAsyncThunk<Returned, ThunkArg = void, State = unknown, Extra = unknown, DispatchType extends Dispatch = Dispatch, ActionType extends string = string, ThunkAPI extends BaseThunkAPI<any, any, any> = BaseThunkAPI<State, Extra, DispatchType>>(type: ActionType, payloadCreator: (arg: ThunkArg, thunkAPI: ThunkAPI) => Promise<Returned> | Returned): ((arg: ThunkArg) => (dispatch: ThunkAPI["dispatch"], getState: ThunkAPI["getState"], extra: ThunkAPI["extra"]) => Promise<PayloadAction<undefined, string, {
+export function createAsyncThunk<Returned, ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {}>(type: string, payloadCreator: (arg: ThunkArg, thunkAPI: GetThunkAPI<ThunkApiConfig>) => Promise<Returned> | Returned): ((arg: ThunkArg) => (dispatch: GetDispatch<ThunkApiConfig>, getState: () => GetState<ThunkApiConfig>, extra: GetExtra<ThunkApiConfig>) => Promise<PayloadAction<undefined, string, {
     arg: ThunkArg;
     requestId: string;
     aborted: boolean;
diff --git a/src/createAsyncThunk.ts b/src/createAsyncThunk.ts
index f0a3726c..bdb3da93 100644
--- a/src/createAsyncThunk.ts
+++ b/src/createAsyncThunk.ts
@@ -1,10 +1,12 @@
-import { Dispatch } from 'redux'
+import { Dispatch, AnyAction } from 'redux'
 import nanoid from 'nanoid'
 import {
   createAction,
   PayloadAction,
   ActionCreatorWithPreparedPayload
 } from './createAction'
+import { ThunkDispatch } from 'redux-thunk'
+import { FallbackIfUnknown } from './tsHelpers'
 
 // @ts-ignore we need the import of these types due to a bundling issue.
 type _Keep = PayloadAction | ActionCreatorWithPreparedPayload<any, unknown>
@@ -50,6 +52,39 @@ export const miniSerializeError = (value: any): any => {
   return value
 }
 
+type AsyncThunkConfig = {
+  state?: unknown
+  dispatch?: Dispatch
+  extra?: unknown
+}
+
+type GetState<ThunkApiConfig> = ThunkApiConfig extends {
+  state: infer State
+}
+  ? State
+  : unknown
+type GetExtra<ThunkApiConfig> = ThunkApiConfig extends { extra: infer Extra }
+  ? Extra
+  : unknown
+type GetDispatch<ThunkApiConfig> = ThunkApiConfig extends {
+  dispatch: infer Dispatch
+}
+  ? FallbackIfUnknown<
+      Dispatch,
+      ThunkDispatch<
+        GetState<ThunkApiConfig>,
+        GetExtra<ThunkApiConfig>,
+        AnyAction
+      >
+    >
+  : ThunkDispatch<GetState<ThunkApiConfig>, GetExtra<ThunkApiConfig>, AnyAction>
+
+type GetThunkAPI<ThunkApiConfig> = BaseThunkAPI<
+  GetState<ThunkApiConfig>,
+  GetExtra<ThunkApiConfig>,
+  GetDispatch<ThunkApiConfig>
+>
+
 /**
  *
  * @param type
@@ -60,20 +95,12 @@ export const miniSerializeError = (value: any): any => {
 export function createAsyncThunk<
   Returned,
   ThunkArg = void,
-  State = unknown,
-  Extra = unknown,
-  DispatchType extends Dispatch = Dispatch,
-  ActionType extends string = string,
-  ThunkAPI extends BaseThunkAPI<any, any, any> = BaseThunkAPI<
-    State,
-    Extra,
-    DispatchType
-  >
+  ThunkApiConfig extends AsyncThunkConfig = {}
 >(
-  type: ActionType,
+  type: string,
   payloadCreator: (
     arg: ThunkArg,
-    thunkAPI: ThunkAPI
+    thunkAPI: GetThunkAPI<ThunkApiConfig>
   ) => Promise<Returned> | Returned
 ) {
   const fulfilled = createAction(
@@ -115,9 +142,9 @@ export function createAsyncThunk<
 
   function actionCreator(arg: ThunkArg) {
     return (
-      dispatch: ThunkAPI['dispatch'],
-      getState: ThunkAPI['getState'],
-      extra: ThunkAPI['extra']
+      dispatch: GetDispatch<ThunkApiConfig>,
+      getState: () => GetState<ThunkApiConfig>,
+      extra: GetExtra<ThunkApiConfig>
     ) => {
       const requestId = nanoid()
       const abortController = new AbortController()
@@ -145,7 +172,7 @@ export function createAsyncThunk<
               extra,
               requestId,
               signal: abortController.signal
-            } as ThunkAPI),
+            }),
             requestId,
             arg
           )
diff --git a/src/tsHelpers.ts b/src/tsHelpers.ts
index d5dcda74..d7c56d30 100644
--- a/src/tsHelpers.ts
+++ b/src/tsHelpers.ts
@@ -20,6 +20,8 @@ export type IsUnknown<T, True, False = never> = unknown extends T
   ? IsAny<T, False, True>
   : False
 
+export type FallbackIfUnknown<T, Fallback> = IsUnknown<T, Fallback, T>
+
 /**
  * @internal
  */


Test patch:
diff --git a/src/combinedTest.test.ts b/src/combinedTest.test.ts
index c66fa21b..187d25fe 100644
--- a/src/combinedTest.test.ts
+++ b/src/combinedTest.test.ts
@@ -1,5 +1,4 @@
-import { Dispatch } from 'redux'
-import { createAsyncThunk, BaseThunkAPI } from './createAsyncThunk'
+import { createAsyncThunk } from './createAsyncThunk'
 import { createAction, PayloadAction } from './createAction'
 import { createSlice } from './createSlice'
 import { configureStore } from './configureStore'
@@ -38,7 +37,9 @@ describe('Combined entity slice', () => {
     const fetchBooksTAC = createAsyncThunk<
       BookModel[],
       void,
-      { books: BooksState }
+      {
+        state: { books: BooksState }
+      }
     >(
       'books/fetch',
       async (arg, { getState, dispatch, extra, requestId, signal }) => {
diff --git a/type-tests/files/createAsyncThunk.typetest.ts b/type-tests/files/createAsyncThunk.typetest.ts
index aa2b3b5b..58f0bc22 100644
--- a/type-tests/files/createAsyncThunk.typetest.ts
+++ b/type-tests/files/createAsyncThunk.typetest.ts
@@ -6,12 +6,10 @@ import { unwrapResult } from 'src/createAsyncThunk'
 function expectType<T>(t: T) {
   return t
 }
-function fn() {}
+const defaultDispatch = (() => {}) as ThunkDispatch<{}, any, AnyAction>
 
-// basic usage
+  // basic usage
 ;(async function() {
-  const dispatch = fn as ThunkDispatch<{}, any, AnyAction>
-
   const async = createAsyncThunk('test', (id: number) =>
     Promise.resolve(id * 2)
   )
@@ -31,7 +29,7 @@ function fn() {}
       })
   )
 
-  const promise = dispatch(async(3))
+  const promise = defaultDispatch(async(3))
   const result = await promise
 
   if (async.fulfilled.match(result)) {
@@ -70,12 +68,20 @@ function fn() {}
     { id: 'a', title: 'First' }
   ]
 
+  const correctDispatch = (() => {}) as ThunkDispatch<
+    BookModel[],
+    { userAPI: Function },
+    AnyAction
+  >
+
   // Verify that the the first type args to createAsyncThunk line up right
   const fetchBooksTAC = createAsyncThunk<
     BookModel[],
     number,
-    BooksState,
-    { userAPI: Function }
+    {
+      state: BooksState
+      extra: { userAPI: Function }
+    }
   >(
     'books/fetch',
     async (arg, { getState, dispatch, extra, requestId, signal }) => {
@@ -87,4 +93,8 @@ function fn() {}
       return fakeBooks
     }
   )
+
+  correctDispatch(fetchBooksTAC(1))
+  // typings:expect-error
+  defaultDispatch(fetchBooksTAC(1))
 })()