Repository: reduxjs/redux-toolkit (ts)

Issue:
createAsyncThunk cannot pass non-serializable arg
I try to use `createAsyncThunk` with non-serializable object like:

```ts
export const roundAsync = createAsyncThunk(
  "someAsync",
  ({ api }: { api: ApiClient }) => {
    return api.apiAction()
  }
)
```

and I got error

```
index.js:1 A non-serializable value was detected in an action, in the path: `meta.arg.api`. Value: ApiClient {} 
Take a look at the logic that dispatched this action:
```

I found this issue ignored `meta.args` by default
https://github.com/reduxjs/redux-toolkit/pull/410

but maybe `createAsyncThunk` fire `meta.arg`

Additionaly, I try  customize `serializableCheck.ignoredPaths` option, I cannot resolve this too.
This maybe not passed `ignorePaths` params  to `findNonSerializableValue`
https://github.com/reduxjs/redux-toolkit/blob/a1e8becbb2e4653d721bdc1d2363a45562c526a2/src/serializableStateInvariantMiddleware.ts#L157-L162

Pull request:
add ignoredActionPaths to createSerializableStateInvariantMiddleware
allow configuration of createSerializableStateInvariantMiddleware ignoredActionPaths, update default value, add tests

fixes #453

I've opted for the additional `ignoredActionPaths` option now, but I'm open for discussion :)

Patch:
diff --git a/docs/api/createEntityAdapter.md b/docs/api/createEntityAdapter.md
index ca701026..0b8d4421 100644
--- a/docs/api/createEntityAdapter.md
+++ b/docs/api/createEntityAdapter.md
@@ -324,7 +324,7 @@ const booksSlice = createSlice({
     booksReceived(state, action) {
       if (state.loading === 'pending') {
         // Or, call them as "mutating" helpers in a case reducer
-        booksAdapter.setAll(state, action.payload.books)
+        booksAdapter.setAll(state, action.payload)
         state.loading = 'idle'
       }
     },
@@ -355,7 +355,7 @@ store.dispatch(bookAdded({ id: 'a', title: 'First' }))
 console.log(store.getState().books)
 // {ids: ["a"], entities: {a: {id: "a", title: "First"}}, loading: 'idle' }
 
-store.dispatch(bookUpdated({ id: 'a', title: 'First (altered)' }))
+store.dispatch(bookUpdated({ id: 'a', changes: { title: 'First (altered)' } }))
 store.dispatch(booksLoading())
 console.log(store.getState().books)
 // {ids: ["a"], entities: {a: {id: "a", title: "First (altered)"}}, loading: 'pending' }
diff --git a/docs/api/createReducer.md b/docs/api/createReducer.md
index 7b2df101..b5024a06 100644
--- a/docs/api/createReducer.md
+++ b/docs/api/createReducer.md
@@ -36,8 +36,10 @@ const counterReducer = createReducer(0, {
 ```
 
 Action creators that were generated using [`createAction`](./createAction.md) may be used directly as the keys here, using
-computed property syntax. (If you are using TypeScript, you may have to use `actionCreator.type` or `actionCreator.toString()`
-to force the TS compiler to accept the computed property.)
+computed property syntax.
+
+> **Note**: If you are using TypeScript, we recommend using the `builder callback` API that is shown below. If you do not use the `builder callback` and are using TypeScript, you will need to use `actionCreator.type` or `actionCreator.toString()`
+> to force the TS compiler to accept the computed property. Please see [Usage With TypeScript](./../usage/usage-with-typescript.md#type-safety-with-extraReducers) for further details.
 
 ```js
 const increment = createAction('increment')
diff --git a/docs/api/createSlice.md b/docs/api/createSlice.md
index 7945f155..bcc17c12 100644
--- a/docs/api/createSlice.md
+++ b/docs/api/createSlice.md
@@ -70,14 +70,47 @@ If two fields from `reducers` and `extraReducers` happen to end up with the same
 the function from `reducers` will be used to handle that action type.
 
 Action creators that were generated using [`createAction`](./createAction.md) may be used directly as the keys here, using
-computed property syntax. (If you are using TypeScript, you may have to use `actionCreator.type` or `actionCreator.toString()`
-to force the TS compiler to accept the computed property.)
+computed property syntax.
+
+```js
+const incrementBy = createAction('incrementBy')
+
+createSlice({
+  name: 'counter',
+  initialState: 0,
+  reducers: {},
+  extraReducers: {
+    [incrementBy]: (state, action) => {
+      return state + action.payload
+    }
+  }
+})
+```
+
+> **Note**: If you are using TypeScript, we recommend using the `builder callback` API that is shown below. If you do not use the `builder callback` and are using TypeScript, you will need to use `actionCreator.type` or `actionCreator.toString()`
+> to force the TS compiler to accept the computed property. Please see [Usage With TypeScript](./../usage/usage-with-typescript.md#type-safety-with-extraReducers) for further details.
 
 ### The "builder callback" API for `extraReducers`
 
 Instead of using a simple object as `extraReducers`, you can also use a callback that receives a `ActionReducerMapBuilder` instance.
 
-We recommend using this API if stricter type safety is necessary when defining reducer argument objects.
+```typescript
+const incrementBy = createAction<number>('incrementBy')
+
+createSlice({
+  name: 'counter',
+  initialState: 0,
+  reducers: {},
+  extraReducers: builder => {
+    builder.addCase(incrementBy, (state, action) => {
+      // action is inferred correctly here with `action.payload` as a `number`
+      return state + action.payload
+    })
+  }
+})
+```
+
+We recommend using this API if stricter type safety is necessary when defining reducer argument objects. It's particularly useful for working with actions produced by `createAction` and `createAsyncThunk`.
 
 ## Return Value
 
@@ -116,6 +149,7 @@ import { createSlice, createAction, PayloadAction } from '@reduxjs/toolkit'
 import { createStore, combineReducers } from 'redux'
 
 const incrementBy = createAction<number>('incrementBy')
+const decrementBy = createAction<number>('decrementBy')
 
 const counter = createSlice({
   name: 'counter',
@@ -128,11 +162,15 @@ const counter = createSlice({
       prepare: (value: number) => ({ payload: value || 2 }) // fallback if the payload is a falsy value
     }
   },
-  // "builder callback API"
-  extraReducers: builder =>
+  // "builder callback API", recommended for TypeScript users
+  extraReducers: builder => {
     builder.addCase(incrementBy, (state, action) => {
       return state + action.payload
     })
+    builder.addCase(decrementBy, (state, action) => {
+      return state - action.payload
+    })
+  }
 })
 
 const user = createSlice({
diff --git a/docs/tutorials/advanced-tutorial.md b/docs/tutorials/advanced-tutorial.md
index 34b3f096..3a5a4b96 100644
--- a/docs/tutorials/advanced-tutorial.md
+++ b/docs/tutorials/advanced-tutorial.md
@@ -733,7 +733,7 @@ export const IssuesListPage = ({
 
 In `<IssuesListPage>`, we import the new `fetchIssuesCount` thunk, and rewrite the component to read the open issues count value from the Redux store.
 
-Inside our `useEffect`, we drop the `fetchIssueCount` function, and dispatch `fetchIssueCount` instead.
+Inside our `useEffect`, we drop the `fetchIssueCount` function, and dispatch `fetchIssuesCount` instead.
 
 ### Logic for Fetching Issues for a Repo
 
diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md
index 52d1bb1f..dd9dd679 100644
--- a/etc/redux-toolkit.api.md
+++ b/etc/redux-toolkit.api.md
@@ -271,6 +271,7 @@ export { Selector }
 // @public
 export interface SerializableStateInvariantMiddlewareOptions {
     getEntries?: (value: any) => [string, any][];
+    ignoredActionPaths?: string[];
     ignoredActions?: string[];
     ignoredPaths?: string[];
     isSerializable?: (value: any) => boolean;
@@ -307,6 +308,8 @@ export type SliceCaseReducers<State> = {
 
 export { ThunkAction }
 
+export { ThunkDispatch }
+
 // @alpha (undocumented)
 export function unwrapResult<R extends ActionTypesWithOptionalErrorAction>(returned: R): PayloadForActionTypesExcludingErrorActions<R>;
 
diff --git a/src/index.ts b/src/index.ts
index ee456cb0..c31d3048 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -8,7 +8,7 @@ export {
   OutputSelector,
   ParametricSelector
 } from 'reselect'
-export { ThunkAction } from 'redux-thunk'
+export { ThunkAction, ThunkDispatch } from 'redux-thunk'
 
 // We deliberately enable Immer's ES5 support, on the grounds that
 // we assume RTK will be used with React Native and other Proxy-less
diff --git a/src/serializableStateInvariantMiddleware.ts b/src/serializableStateInvariantMiddleware.ts
index a4955425..a69aaad2 100644
--- a/src/serializableStateInvariantMiddleware.ts
+++ b/src/serializableStateInvariantMiddleware.ts
@@ -36,7 +36,7 @@ export function findNonSerializableValue(
   path: ReadonlyArray<string> = [],
   isSerializable: (value: unknown) => boolean = isPlain,
   getEntries?: (value: unknown) => [string, any][],
-  ignoredPaths: string[] = ['meta.args']
+  ignoredPaths: string[] = []
 ): NonSerializableValue | false {
   let foundNestedSerializable: NonSerializableValue | false
 
@@ -111,6 +111,11 @@ export interface SerializableStateInvariantMiddlewareOptions {
    */
   ignoredActions?: string[]
 
+  /**
+   * An array of dot-separated path strings to ignore when checking for serializability, Defaults to ['meta.arg']
+   */
+  ignoredActionPaths?: string[]
+
   /**
    * An array of dot-separated path strings to ignore when checking for serializability, Defaults to []
    */
@@ -140,6 +145,7 @@ export function createSerializableStateInvariantMiddleware(
     isSerializable = isPlain,
     getEntries,
     ignoredActions = [],
+    ignoredActionPaths = ['meta.arg'],
     ignoredPaths = [],
     warnAfter = 32
   } = options
@@ -158,7 +164,8 @@ export function createSerializableStateInvariantMiddleware(
         action,
         [],
         isSerializable,
-        getEntries
+        getEntries,
+        ignoredActionPaths
       )
 
       if (foundActionNonSerializableValue) {


Test patch:
diff --git a/src/createAsyncThunk.test.ts b/src/createAsyncThunk.test.ts
index c1a5c150..dd50f78d 100644
--- a/src/createAsyncThunk.test.ts
+++ b/src/createAsyncThunk.test.ts
@@ -463,3 +463,16 @@ describe('createAsyncThunk with abortController', () => {
     })
   })
 })
+
+test('non-serializable arguments are ignored by serializableStateInvariantMiddleware', async () => {
+  const restore = mockConsole(createConsole())
+  const nonSerializableValue = new Map()
+  const asyncThunk = createAsyncThunk('test', (arg: Map<any, any>) => {})
+
+  configureStore({
+    reducer: () => 0
+  }).dispatch(asyncThunk(nonSerializableValue))
+
+  expect(getLog().log).toMatchInlineSnapshot(`""`)
+  restore()
+})
diff --git a/src/serializableStateInvariantMiddleware.test.ts b/src/serializableStateInvariantMiddleware.test.ts
index c435b659..a512ed50 100644
--- a/src/serializableStateInvariantMiddleware.test.ts
+++ b/src/serializableStateInvariantMiddleware.test.ts
@@ -330,6 +330,61 @@ describe('serializableStateInvariantMiddleware', () => {
     expect(numTimesCalled).toBeGreaterThan(0)
   })
 
+  describe('ignored action paths', () => {
+    function reducer() {
+      return 0
+    }
+    const nonSerializableValue = new Map()
+
+    it('default value: meta.arg', () => {
+      configureStore({
+        reducer,
+        middleware: [createSerializableStateInvariantMiddleware()]
+      }).dispatch({ type: 'test', meta: { arg: nonSerializableValue } })
+
+      expect(getLog().log).toMatchInlineSnapshot(`""`)
+    })
+
+    it('default value can be overridden', () => {
+      configureStore({
+        reducer,
+        middleware: [
+          createSerializableStateInvariantMiddleware({
+            ignoredActionPaths: []
+          })
+        ]
+      }).dispatch({ type: 'test', meta: { arg: nonSerializableValue } })
+
+      expect(getLog().log).toMatchInlineSnapshot(`
+        "A non-serializable value was detected in an action, in the path: \`meta.arg\`. Value: Map {} 
+        Take a look at the logic that dispatched this action:  Object {
+          \\"meta\\": Object {
+            \\"arg\\": Map {},
+          },
+          \\"type\\": \\"test\\",
+        } 
+        (See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)"
+      `)
+    })
+
+    it('can specify (multiple) different values', () => {
+      configureStore({
+        reducer,
+        middleware: [
+          createSerializableStateInvariantMiddleware({
+            ignoredActionPaths: ['payload', 'meta.arg']
+          })
+        ]
+      }).dispatch({
+        type: 'test',
+        payload: { arg: nonSerializableValue },
+        meta: { arg: nonSerializableValue }
+      })
+
+      expect(getLog().log).toMatchInlineSnapshot(`""`)
+    })
+  })
+
   it('should not check serializability for ignored slice names', () => {
     const ACTION_TYPE = 'TEST_ACTION'
 
@@ -386,29 +441,6 @@ describe('serializableStateInvariantMiddleware', () => {
     `)
   })
 
-  it('should not check serializability for meta.args by default', () => {
-    const badValue = new Map()
-
-    const reducer: Reducer = (state = 42, action) => {
-      return state
-    }
-
-    const serializableStateInvariantMiddleware = createSerializableStateInvariantMiddleware()
-
-    const store = configureStore({
-      reducer: {
-        testSlice: reducer
-      },
-      middleware: [serializableStateInvariantMiddleware]
-    })
-
-    store.dispatch({ type: 'testAction', meta: { args: { badValue } } })
-
-    const { log } = getLog()
-    expect(log).toBe('')
-    const q = 42
-  })
-
   it('Should print a warning if execution takes too long', () => {
     const reducer: Reducer = (state = 42, action) => {
       return state