Repository: reduxjs/redux-toolkit

Problem statement:
Suggestion: Define thunk on createSlice
I'm sure this has already been discussed elsewhere, but perhaps not with the following reasoning in mind, so please bear with me :)

One thing that I often come across is the desire to reuse a whole reducer across different parts of the state, so different slices would be the correct term I guess. 
This can of course easily be done by creating a higher order reducer etc. What cannot so easily be done however is to reuse the async actions. You need to somehow point the action back to the different part of the state, and you also need to make sure any getState calls inside any async actions select from the correct part of the state.

Couldn't this perhaps somehow be baked into createSlice? Apologies for not having a fully fledged idea here, but I do think this could be a nice improvement to already excellent DX of RTK. Especially if those thunk actions could also somehow easily have the "local" state available, so that they don't need to know where in the global state they are located.

Some other things that would be improved by defining thunks via createSlice (which I'm sure you are of course alredy aware of) are:
- Remove the need to define the name in a way like this:
      const addGroup = createAsyncThunk(`${sliceName}/addGroup`, async ({group}, {getState}) => {
    });
- Automatically export all thunks as well via reducer.actions

Would be interested to hear your thoughts on this. And thanks again for the awesome library 👍 


Hints:
We would desperately like to do this, and we even had a prototype. Unfortunately, it turned out to be basically impossible due to circular TS type definitions.  Thunks require knowledge of `RootState` for `getState`, which is formed by looking at the slice reducer state type. But, if you try to define thunks in the slice, now the slice needs to know `RootState`, and you've got yourself a circular type definition.

See https://github.com/reduxjs/redux-toolkit/pull/637 for the failed PR attempt.
Adding to that, we hope that the general need for such "higher order slices", especially in combination with `createAsyncThunk` will be reduced quite a bit once [RTK-Query](https://rtk-query-docs.netlify.app/) is part of the next release of RTK.
Ok. A real shame that TS gets in the way of useful stuff like this though.

Thanks
> Adding to that, we hope that the general need for such "higher order slices", especially in combination with `createAsyncThunk` will be reduced quite a bit once [RTK-Query](https://rtk-query-docs.netlify.app/) is part of the next release of RTK.

Right. Will have to start looking closer into at RTK Query at some point, but its quite a leap from basic createSlice usage I guess...
If those slices were used for "server data cache" purposes only, primarily deleting code ;)
It is used for displaying and updating a list of entities, so I guess so :)
Perhaps a recipe for converting a slice reducer like this to RTK query could be added to the docs when it is released?
Probably worth showing a switch from standalone thunks + reducers to RTK Query, yeah.  But as Lenz said - it's mostly "delete your existing code and just use the auto-generated hook" :)
It's literally 
* set up an api with `createAPI`
```diff
+export const pokemonApi = createApi({
+  reducerPath: 'pokemonApi',
+  baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
+  endpoints: (builder) => ({
+	// endpoints here
+  }),
+});

export const store = configureStore({
  reducer: {
    // Add the generated reducer as a specific top-level slice
+   [pokemonApi.reducerPath]: pokemonApi.reducer,
  },
  // Adding the api middleware enables caching, invalidation, polling,
  // and other useful features of `rtk-query`.
+ middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(pokemonApi.middleware),
});
```
* follow the RTK query docs to add an endpoint
```diff
  endpoints: (builder) => ({
-	// endpoints here
+    getPokemonByName: builder.query({
+      query: (name: string) => `pokemon/${name}`,
+    }),
  })
```
* use the auto-generated hook
```js
const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur');
```
* delete your slice


There is no real "migration" apart from "build from zero, delete" as they work differently and one just replaces the other.
Looks good.

This is probably described somewhere, but I assume that RTK Query allows you to plug in your own "fetcher" of sorts? We use a wrapper for native browser fetch, in order to pass some headers etc.
That is `baseQuery` with the `fetchBaseQuery` in this example. There are examples for usage with axios or graphql-request buried in the docs and/or examples.
(and we will be adding a docs page specifically to cover using a customized base query)
Sounds good. Fingers crossed something like this might make it to plain RTK createSlice though. Forgot to mention that somehow involving selectors in the createSlice utility as well would be even better :)

Anyway, look forward to trying out RTK Query a bit more.
Selectors would unfortunately suffer from the exact same typing issue. You need the `RootState` to type them correctly, but the `RootState` type is derived from the slice state type.

I believe Lenz said it's possible to break the cycle if you explicitly do `export default todosSlice.reducer as Reducer<TodosState>`, but that's not a pattern we've encouraged so far, and we're trying to stick with suggested patterns that "just work" as much as possible.
Isolated slice-specific selectors would be totally possible using a pattern like `entityAdapter`, actually I got that on my TODO list, but RTKQ is eating quite a lot time right now :D

Could potentially look like this:

```ts
const slice = createSlice({
  // ...,
  selectors: {
    foo: state => state.foo,
    bar: state => state.bar,
    fooBar: state => createSelector(slice.selectors.foo, slice.selectors.bar, (foo, bar) => foo + bar)
  }
})

export { selectFoo, selectBar, selectFooBar } = slice.bindSelectors(store => store.mySlice)
```
> Isolated slice-specific selectors would be totally possible using a pattern like `entityAdapter`, actually I got that on my TODO list, but RTKQ is eating quite a lot time right now :D
> 
> Could potentially look like this:
> 
> ```ts
> const slice = createSlice({
>   // ...,
>   selectors: {
>     foo: state => state.foo,
>     bar: state => state.bar,
>     fooBar: state => createSelector(slice.selectors.foo, slice.selectors.bar, (foo, bar) => foo + bar)
>   }
> })
> 
> export { selectFoo, selectBar, selectFooBar } = slice.bindSelectors(store => store.mySlice)
> ```

Would be awesome to have something like this built in. Looks very similar to what we are doing already in our own custom way, except the selectors are bound to the global store on the top-level instead (since the slice doesn't necessarily know or control where in the store it ends up).
Definitely not happening any time soon, unfortunately :(

Patch:
diff --git a/packages/toolkit/src/createAsyncThunk.ts b/packages/toolkit/src/createAsyncThunk.ts
--- a/packages/toolkit/src/createAsyncThunk.ts
+++ b/packages/toolkit/src/createAsyncThunk.ts
@@ -105,7 +105,7 @@ export const miniSerializeError = (value: any): SerializedError => {
   return { message: String(value) }
 }
 
-type AsyncThunkConfig = {
+export type AsyncThunkConfig = {
   state?: unknown
   dispatch?: Dispatch
   extra?: unknown
@@ -414,7 +414,7 @@ export type AsyncThunk<
   typePrefix: string
 }
 
-type OverrideThunkApiConfigs<OldConfig, NewConfig> = Id<
+export type OverrideThunkApiConfigs<OldConfig, NewConfig> = Id<
   NewConfig & Omit<OldConfig, keyof NewConfig>
 >
 
diff --git a/packages/toolkit/src/createSlice.ts b/packages/toolkit/src/createSlice.ts
--- a/packages/toolkit/src/createSlice.ts
+++ b/packages/toolkit/src/createSlice.ts
@@ -1,4 +1,4 @@
-import type { AnyAction, Reducer } from 'redux'
+import type { Action, AnyAction, Reducer } from 'redux'
 import type {
   ActionCreatorWithoutPayload,
   PayloadAction,
@@ -18,6 +18,14 @@ import { executeReducerBuilderCallback } from './mapBuilders'
 import type { Id, NoInfer, Tail } from './tsHelpers'
 import { freezeDraftable } from './utils'
 import type { CombinedSliceReducer, InjectConfig } from './combineSlices'
+import type {
+  AsyncThunk,
+  AsyncThunkConfig,
+  AsyncThunkOptions,
+  AsyncThunkPayloadCreator,
+  OverrideThunkApiConfigs,
+} from './createAsyncThunk'
+import { createAsyncThunk } from './createAsyncThunk'
 
 /**
  * The return value of `createSlice`
@@ -167,7 +175,9 @@ export interface CreateSliceOptions<
    * functions. For every action type, a matching action creator will be
    * generated using `createAction()`.
    */
-  reducers: ValidateSliceCaseReducers<State, CR>
+  reducers:
+    | ValidateSliceCaseReducers<State, CR>
+    | ((creators: ReducerCreators<State>) => CR)
 
   /**
    * A callback that receives a *builder* object to define
@@ -211,7 +221,7 @@ createSlice({
 })
 ```
    */
-  extraReducers?: (builder: ActionReducerMapBuilder<NoInfer<State>>) => void
+  extraReducers?: (builder: ActionReducerMapBuilder<State>) => void
 
   /**
    * A map of selectors that receive the slice's state and any additional arguments, and return a result.
@@ -219,6 +229,21 @@ createSlice({
   selectors?: Selectors
 }
 
+const reducerDefinitionType: unique symbol = Symbol.for('rtk-reducer-type')
+enum ReducerType {
+  reducer = 'reducer',
+  reducerWithPrepare = 'reducerWithPrepare',
+  asyncThunk = 'asyncThunk',
+}
+
+interface ReducerDefinition<T extends ReducerType = ReducerType> {
+  [reducerDefinitionType]: T
+}
+
+export interface CaseReducerDefinition<S = any, A extends Action = AnyAction>
+  extends CaseReducer<S, A>,
+    ReducerDefinition<ReducerType.reducer> {}
+
 /**
  * A CaseReducer with a `prepare` method.
  *
@@ -229,16 +254,147 @@ export type CaseReducerWithPrepare<State, Action extends PayloadAction> = {
   prepare: PrepareAction<Action['payload']>
 }
 
+export interface CaseReducerWithPrepareDefinition<
+  State,
+  Action extends PayloadAction
+> extends CaseReducerWithPrepare<State, Action>,
+    ReducerDefinition<ReducerType.reducerWithPrepare> {}
+
+export interface AsyncThunkSliceReducerConfig<
+  State,
+  ThunkArg extends any,
+  Returned = unknown,
+  ThunkApiConfig extends AsyncThunkConfig = {}
+> {
+  pending?: CaseReducer<
+    State,
+    ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['pending']>
+  >
+  rejected?: CaseReducer<
+    State,
+    ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['rejected']>
+  >
+  fulfilled?: CaseReducer<
+    State,
+    ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['fulfilled']>
+  >
+  options?: AsyncThunkOptions<ThunkArg, ThunkApiConfig>
+}
+
+export interface AsyncThunkSliceReducerDefinition<
+  State,
+  ThunkArg extends any,
+  Returned = unknown,
+  ThunkApiConfig extends AsyncThunkConfig = {}
+> extends AsyncThunkSliceReducerConfig<
+      State,
+      ThunkArg,
+      Returned,
+      ThunkApiConfig
+    >,
+    ReducerDefinition<ReducerType.asyncThunk> {
+  payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>
+}
+
+/**
+ * Providing these as part of the config would cause circular types, so we disallow passing them
+ */
+type PreventCircular<ThunkApiConfig> = {
+  [K in keyof ThunkApiConfig]: K extends 'state' | 'dispatch'
+    ? never
+    : ThunkApiConfig[K]
+}
+
+interface AsyncThunkCreator<
+  State,
+  CurriedThunkApiConfig extends PreventCircular<AsyncThunkConfig> = PreventCircular<AsyncThunkConfig>
+> {
+  <ThunkArg extends any, Returned = unknown>(
+    payloadCreator: AsyncThunkPayloadCreator<
+      Returned,
+      ThunkArg,
+      CurriedThunkApiConfig
+    >,
+    config?: AsyncThunkSliceReducerConfig<
+      State,
+      ThunkArg,
+      Returned,
+      CurriedThunkApiConfig
+    >
+  ): AsyncThunkSliceReducerDefinition<
+    State,
+    ThunkArg,
+    Returned,
+    CurriedThunkApiConfig
+  >
+  <
+    ThunkArg extends any,
+    Returned = unknown,
+    ThunkApiConfig extends PreventCircular<AsyncThunkConfig> = {}
+  >(
+    payloadCreator: AsyncThunkPayloadCreator<
+      Returned,
+      ThunkArg,
+      ThunkApiConfig
+    >,
+    config?: AsyncThunkSliceReducerConfig<
+      State,
+      ThunkArg,
+      Returned,
+      ThunkApiConfig
+    >
+  ): AsyncThunkSliceReducerDefinition<State, ThunkArg, Returned, ThunkApiConfig>
+  withTypes<
+    ThunkApiConfig extends PreventCircular<AsyncThunkConfig>
+  >(): AsyncThunkCreator<
+    State,
+    OverrideThunkApiConfigs<CurriedThunkApiConfig, ThunkApiConfig>
+  >
+}
+
+export interface ReducerCreators<State> {
+  reducer<Payload>(
+    caseReducer: CaseReducer<State, PayloadAction<Payload>>
+  ): CaseReducerDefinition<State, PayloadAction<Payload>>
+
+  asyncThunk: AsyncThunkCreator<State>
+
+  preparedReducer<Prepare extends PrepareAction<any>>(
+    prepare: Prepare,
+    reducer: CaseReducer<
+      State,
+      ReturnType<_ActionCreatorWithPreparedPayload<Prepare>>
+    >
+  ): {
+    [reducerDefinitionType]: ReducerType.reducerWithPrepare
+    prepare: Prepare
+    reducer: CaseReducer<
+      State,
+      ReturnType<_ActionCreatorWithPreparedPayload<Prepare>>
+    >
+  }
+}
+
 /**
  * The type describing a slice's `reducers` option.
  *
  * @public
  */
-export type SliceCaseReducers<State> = {
-  [K: string]:
-    | CaseReducer<State, PayloadAction<any>>
-    | CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>
-}
+export type SliceCaseReducers<State> =
+  | Record<
+      string,
+      | CaseReducerDefinition<State, PayloadAction<any>>
+      | CaseReducerWithPrepareDefinition<
+          State,
+          PayloadAction<any, string, any, any>
+        >
+      | AsyncThunkSliceReducerDefinition<State, any, any, any>
+    >
+  | Record<
+      string,
+      | CaseReducer<State, PayloadAction<any>>
+      | CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>
+    >
 
 /**
  * The type describing a slice's `selectors` option.
@@ -261,15 +417,29 @@ export type CaseReducerActions<
   CaseReducers extends SliceCaseReducers<any>,
   SliceName extends string
 > = {
-  [Type in keyof CaseReducers]: CaseReducers[Type] extends { prepare: any }
-    ? ActionCreatorForCaseReducerWithPrepare<
-        CaseReducers[Type],
-        SliceActionType<SliceName, Type>
-      >
-    : ActionCreatorForCaseReducer<
-        CaseReducers[Type],
-        SliceActionType<SliceName, Type>
-      >
+  [Type in keyof CaseReducers]: CaseReducers[Type] extends infer Definition
+    ? Definition extends { prepare: any }
+      ? ActionCreatorForCaseReducerWithPrepare<
+          Definition,
+          SliceActionType<SliceName, Type>
+        >
+      : Definition extends AsyncThunkSliceReducerDefinition<
+          any,
+          infer ThunkArg,
+          infer Returned,
+          infer ThunkApiConfig
+        >
+      ? AsyncThunk<Returned, ThunkArg, ThunkApiConfig>
+      : Definition extends { reducer: any }
+      ? ActionCreatorForCaseReducer<
+          Definition['reducer'],
+          SliceActionType<SliceName, Type>
+        >
+      : ActionCreatorForCaseReducer<
+          Definition,
+          SliceActionType<SliceName, Type>
+        >
+    : never
 }
 
 /**
@@ -303,11 +473,15 @@ type ActionCreatorForCaseReducer<CR, Type extends string> = CR extends (
  * @internal
  */
 type SliceDefinedCaseReducers<CaseReducers extends SliceCaseReducers<any>> = {
-  [Type in keyof CaseReducers]: CaseReducers[Type] extends {
-    reducer: infer Reducer
-  }
-    ? Reducer
-    : CaseReducers[Type]
+  [Type in keyof CaseReducers]: CaseReducers[Type] extends infer Definition
+    ? Definition extends AsyncThunkSliceReducerDefinition<any, any, any, any>
+      ? Id<Pick<Required<Definition>, 'fulfilled' | 'rejected' | 'pending'>>
+      : Definition extends {
+          reducer: infer Reducer
+        }
+      ? Reducer
+      : Definition
+    : never
 }
 
 /**
@@ -362,8 +536,6 @@ function getType(slice: string, actionKey: string): string {
  * action creators and action types that correspond to the
  * reducers and state.
  *
- * The `reducer` argument is passed to `createReducer()`.
- *
  * @public
  */
 export function createSlice<
@@ -391,38 +563,39 @@ export function createSlice<
     }
   }
 
-  const initialState =
-    typeof options.initialState == 'function'
-      ? options.initialState
-      : freezeDraftable(options.initialState)
-
-  const reducers = options.reducers || {}
+  const reducers =
+    (typeof options.reducers === 'function'
+      ? options.reducers(buildReducerCreators<State>())
+      : options.reducers) || {}
 
   const reducerNames = Object.keys(reducers)
 
-  const sliceCaseReducersByName: Record<string, CaseReducer> = {}
-  const sliceCaseReducersByType: Record<string, CaseReducer> = {}
-  const actionCreators: Record<string, Function> = {}
+  const context: ReducerHandlingContext<State> = {
+    sliceCaseReducersByName: {},
+    sliceCaseReducersByType: {},
+    actionCreators: {},
+  }
 
   reducerNames.forEach((reducerName) => {
-    const maybeReducerWithPrepare = reducers[reducerName]
-    const type = getType(name, reducerName)
-
-    let caseReducer: CaseReducer<State, any>
-    let prepareCallback: PrepareAction<any> | undefined
-
-    if ('reducer' in maybeReducerWithPrepare) {
-      caseReducer = maybeReducerWithPrepare.reducer
-      prepareCallback = maybeReducerWithPrepare.prepare
+    const reducerDefinition = reducers[reducerName]
+    const reducerDetails: ReducerDetails = {
+      reducerName,
+      type: getType(name, reducerName),
+      createNotation: typeof options.reducers === 'function',
+    }
+    if (isAsyncThunkSliceReducerDefinition<State>(reducerDefinition)) {
+      handleThunkCaseReducerDefinition(
+        reducerDetails,
+        reducerDefinition,
+        context
+      )
     } else {
-      caseReducer = maybeReducerWithPrepare
+      handleNormalReducerDefinition<State>(
+        reducerDetails,
+        reducerDefinition,
+        context
+      )
     }
-
-    sliceCaseReducersByName[reducerName] = caseReducer
-    sliceCaseReducersByType[type] = caseReducer
-    actionCreators[reducerName] = prepareCallback
-      ? createAction(type, prepareCallback)
-      : createAction(type)
   })
 
   function buildReducer() {
@@ -442,9 +615,12 @@ export function createSlice<
         ? executeReducerBuilderCallback(options.extraReducers)
         : [options.extraReducers]
 
-    const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType }
+    const finalCaseReducers = {
+      ...extraReducers,
+      ...context.sliceCaseReducersByType,
+    }
 
-    return createReducer(initialState, (builder) => {
+    return createReducer(options.initialState, (builder) => {
       for (let key in finalCaseReducers) {
         builder.addCase(key, finalCaseReducers[key] as CaseReducer<any>)
       }
@@ -481,8 +657,8 @@ export function createSlice<
 
       return _reducer(state, action)
     },
-    actions: actionCreators as any,
-    caseReducers: sliceCaseReducersByName as any,
+    actions: context.actionCreators as any,
+    caseReducers: context.sliceCaseReducersByName as any,
     getInitialState() {
       if (!_reducer) _reducer = buildReducer()
 
@@ -537,3 +713,133 @@ export function createSlice<
   }
   return slice
 }
+
+interface ReducerHandlingContext<State> {
+  sliceCaseReducersByName: Record<
+    string,
+    | CaseReducer<State, any>
+    | Pick<
+        AsyncThunkSliceReducerDefinition<State, any, any, any>,
+        'fulfilled' | 'rejected' | 'pending'
+      >
+  >
+  sliceCaseReducersByType: Record<string, CaseReducer<State, any>>
+  actionCreators: Record<string, Function>
+}
+
+interface ReducerDetails {
+  reducerName: string
+  type: string
+  createNotation: boolean
+}
+
+function buildReducerCreators<State>(): ReducerCreators<State> {
+  function asyncThunk(
+    payloadCreator: AsyncThunkPayloadCreator<any, any>,
+    config: AsyncThunkSliceReducerConfig<State, any>
+  ): AsyncThunkSliceReducerDefinition<State, any> {
+    return {
+      [reducerDefinitionType]: ReducerType.asyncThunk,
+      payloadCreator,
+      ...config,
+    }
+  }
+  asyncThunk.withTypes = () => asyncThunk
+  return {
+    reducer(caseReducer) {
+      return Object.assign(
+        {
+          // hack so the wrapping function has the same name as the original
+          // we need to create a wrapper so the `reducerDefinitionType` is not assigned to the original
+          [caseReducer.name](...args: Parameters<typeof caseReducer>) {
+            return caseReducer(...args)
+          },
+        }[caseReducer.name],
+        {
+          [reducerDefinitionType]: ReducerType.reducer,
+        } as const
+      )
+    },
+    preparedReducer(prepare, reducer) {
+      return {
+        [reducerDefinitionType]: ReducerType.reducerWithPrepare,
+        prepare,
+        reducer,
+      }
+    },
+    asyncThunk: asyncThunk as any,
+  }
+}
+
+function handleNormalReducerDefinition<State>(
+  { type, reducerName, createNotation }: ReducerDetails,
+  maybeReducerWithPrepare:
+    | CaseReducer<State, { payload: any; type: string }>
+    | CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>,
+  context: ReducerHandlingContext<State>
+) {
+  let caseReducer: CaseReducer<State, any>
+  let prepareCallback: PrepareAction<any> | undefined
+  if ('reducer' in maybeReducerWithPrepare) {
+    if (
+      createNotation &&
+      !isCaseReducerWithPrepareDefinition(maybeReducerWithPrepare)
+    ) {
+      throw new Error(
+        'Please use the `create.preparedReducer` notation for prepared action creators with the `create` notation.'
+      )
+    }
+    caseReducer = maybeReducerWithPrepare.reducer
+    prepareCallback = maybeReducerWithPrepare.prepare
+  } else {
+    caseReducer = maybeReducerWithPrepare
+  }
+  context.sliceCaseReducersByName[reducerName] = caseReducer
+  context.sliceCaseReducersByType[type] = caseReducer
+  context.actionCreators[reducerName] = prepareCallback
+    ? createAction(type, prepareCallback)
+    : createAction(type)
+}
+
+function isAsyncThunkSliceReducerDefinition<State>(
+  reducerDefinition: any
+): reducerDefinition is AsyncThunkSliceReducerDefinition<State, any, any, any> {
+  return reducerDefinition[reducerDefinitionType] === ReducerType.asyncThunk
+}
+
+function isCaseReducerWithPrepareDefinition<State>(
+  reducerDefinition: any
+): reducerDefinition is CaseReducerWithPrepareDefinition<State, any> {
+  return (
+    reducerDefinition[reducerDefinitionType] === ReducerType.reducerWithPrepare
+  )
+}
+
+function handleThunkCaseReducerDefinition<State>(
+  { type, reducerName }: ReducerDetails,
+  reducerDefinition: AsyncThunkSliceReducerDefinition<State, any, any, any>,
+  context: ReducerHandlingContext<State>
+) {
+  const { payloadCreator, fulfilled, pending, rejected, options } =
+    reducerDefinition
+  const thunk = createAsyncThunk(type, payloadCreator, options as any)
+  context.actionCreators[reducerName] = thunk
+
+  if (fulfilled) {
+    context.sliceCaseReducersByType[thunk.fulfilled.type] = fulfilled
+  }
+  if (pending) {
+    context.sliceCaseReducersByType[thunk.pending.type] = pending
+  }
+  if (rejected) {
+    context.sliceCaseReducersByType[thunk.rejected.type] = rejected
+  }
+
+  context.sliceCaseReducersByName[reducerName] = {
+    fulfilled: fulfilled || noop,
+    pending: pending || noop,
+    rejected: rejected || noop,
+  }
+}
+
+function noop() {}
diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts
--- a/packages/toolkit/src/index.ts
+++ b/packages/toolkit/src/index.ts
@@ -69,6 +69,7 @@ export type {
   SliceCaseReducers,
   ValidateSliceCaseReducers,
   CaseReducerWithPrepare,
+  ReducerCreators,
 } from './createSlice'
 export {
   // js


Test patch:
diff --git a/packages/toolkit/src/tests/createSlice.test.ts b/packages/toolkit/src/tests/createSlice.test.ts
--- a/packages/toolkit/src/tests/createSlice.test.ts
+++ b/packages/toolkit/src/tests/createSlice.test.ts
@@ -1,7 +1,11 @@
 import { vi } from 'vitest'
 import type { PayloadAction, WithSlice } from '@reduxjs/toolkit'
-import { combineSlices } from '@reduxjs/toolkit'
-import { createSlice, createAction } from '@reduxjs/toolkit'
+import {
+  configureStore,
+  combineSlices,
+  createSlice,
+  createAction,
+} from '@reduxjs/toolkit'
 import {
   mockConsole,
   createConsole,
@@ -553,4 +557,215 @@ describe('createSlice', () => {
       expect(injected2State.injected2).toBe(slice.getInitialState() + 1)
     })
   })
+  describe('reducers definition with asyncThunks', () => {
+    function pending(state: any[], action: any) {
+      state.push(['pendingReducer', action])
+    }
+    function fulfilled(state: any[], action: any) {
+      state.push(['fulfilledReducer', action])
+    }
+    function rejected(state: any[], action: any) {
+      state.push(['rejectedReducer', action])
+    }
+
+    test('successful thunk', async () => {
+      const slice = createSlice({
+        name: 'test',
+        initialState: [] as any[],
+        reducers: (create) => ({
+          thunkReducers: create.asyncThunk(
+            function payloadCreator(arg, api) {
+              return Promise.resolve('resolved payload')
+            },
+            { pending, fulfilled, rejected }
+          ),
+        }),
+      })
+
+      const store = configureStore({
+        reducer: slice.reducer,
+      })
+      await store.dispatch(slice.actions.thunkReducers('test'))
+      expect(store.getState()).toMatchObject([
+        [
+          'pendingReducer',
+          {
+            type: 'test/thunkReducers/pending',
+            payload: undefined,
+          },
+        ],
+        [
+          'fulfilledReducer',
+          {
+            type: 'test/thunkReducers/fulfilled',
+            payload: 'resolved payload',
+          },
+        ],
+      ])
+    })
+
+    test('rejected thunk', async () => {
+      const slice = createSlice({
+        name: 'test',
+        initialState: [] as any[],
+        reducers: (create) => ({
+          thunkReducers: create.asyncThunk(
+            // payloadCreator isn't allowed to return never
+            function payloadCreator(arg, api): any {
+              throw new Error('')
+            },
+            { pending, fulfilled, rejected }
+          ),
+        }),
+      })
+
+      const store = configureStore({
+        reducer: slice.reducer,
+      })
+      await store.dispatch(slice.actions.thunkReducers('test'))
+      expect(store.getState()).toMatchObject([
+        [
+          'pendingReducer',
+          {
+            type: 'test/thunkReducers/pending',
+            payload: undefined,
+          },
+        ],
+        [
+          'rejectedReducer',
+          {
+            type: 'test/thunkReducers/rejected',
+            payload: undefined,
+          },
+        ],
+      ])
+    })
+
+    test('with options', async () => {
+      const slice = createSlice({
+        name: 'test',
+        initialState: [] as any[],
+        reducers: (create) => ({
+          thunkReducers: create.asyncThunk(
+            function payloadCreator(arg, api) {
+              return 'should not call this'
+            },
+            {
+              options: {
+                condition() {
+                  return false
+                },
+                dispatchConditionRejection: true,
+              },
+              pending,
+              fulfilled,
+              rejected,
+            }
+          ),
+        }),
+      })
+
+      const store = configureStore({
+        reducer: slice.reducer,
+      })
+      await store.dispatch(slice.actions.thunkReducers('test'))
+      expect(store.getState()).toMatchObject([
+        [
+          'rejectedReducer',
+          {
+            type: 'test/thunkReducers/rejected',
+            payload: undefined,
+            meta: { condition: true },
+          },
+        ],
+      ])
+    })
+
+    test('has caseReducers for the asyncThunk', async () => {
+      const slice = createSlice({
+        name: 'test',
+        initialState: [],
+        reducers: (create) => ({
+          thunkReducers: create.asyncThunk(
+            function payloadCreator(arg, api) {
+              return Promise.resolve('resolved payload')
+            },
+            { pending, fulfilled }
+          ),
+        }),
+      })
+
+      expect(slice.caseReducers.thunkReducers.pending).toBe(pending)
+      expect(slice.caseReducers.thunkReducers.fulfilled).toBe(fulfilled)
+      // even though it is not defined above, this should at least be a no-op function to match the TypeScript typings
+      // and should be callable as a reducer even if it does nothing
+      expect(() =>
+        slice.caseReducers.thunkReducers.rejected(
+          [],
+          slice.actions.thunkReducers.rejected(
+            new Error('test'),
+            'fakeRequestId',
+            {}
+          )
+        )
+      ).not.toThrow()
+    })
+
+    test('can define reducer with prepare statement using create.preparedReducer', async () => {
+      const slice = createSlice({
+        name: 'test',
+        initialState: [] as any[],
+        reducers: (create) => ({
+          prepared: create.preparedReducer(
+            (p: string, m: number, e: { message: string }) => ({
+              payload: p,
+              meta: m,
+              error: e,
+            }),
+            (state, action) => {
+              state.push(action)
+            }
+          ),
+        }),
+      })
+
+      expect(
+        slice.reducer([], slice.actions.prepared('test', 1, { message: 'err' }))
+      ).toMatchInlineSnapshot(`
+        [
+          {
+            "error": {
+              "message": "err",
+            },
+            "meta": 1,
+            "payload": "test",
+            "type": "test/prepared",
+          },
+        ]
+      `)
+    })
+
+    test('throws an error when invoked with a normal `prepare` object that has not gone through a `create.preparedReducer` call', async () => {
+      expect(() =>
+        createSlice({
+          name: 'test',
+          initialState: [] as any[],
+          reducers: (create) => ({
+            prepared: {
+              prepare: (p: string, m: number, e: { message: string }) => ({
+                payload: p,
+                meta: m,
+                error: e,
+              }),
+              reducer: (state, action) => {
+                state.push(action)
+              },
+            },
+          }),
+        })
+      ).toThrowErrorMatchingInlineSnapshot(
+        `"Please use the \`create.preparedReducer\` notation for prepared action creators with the \`create\` notation."`
+      )
+    })
+  })
 })
diff --git a/packages/toolkit/src/tests/createSlice.typetest.ts b/packages/toolkit/src/tests/createSlice.typetest.ts
--- a/packages/toolkit/src/tests/createSlice.typetest.ts
+++ b/packages/toolkit/src/tests/createSlice.typetest.ts
@@ -6,12 +6,20 @@ import type {
   ActionCreatorWithPayload,
   ActionCreatorWithPreparedPayload,
   ActionReducerMapBuilder,
+  AsyncThunk,
+  CaseReducer,
   PayloadAction,
+  PayloadActionCreator,
+  ReducerCreators,
+  SerializedError,
   SliceCaseReducers,
+  ThunkDispatch,
   ValidateSliceCaseReducers,
 } from '@reduxjs/toolkit'
+import { configureStore } from '@reduxjs/toolkit'
 import { createAction, createSlice } from '@reduxjs/toolkit'
-import { expectType } from './helpers'
+import { expectExactType, expectType, expectUnknown } from './helpers'
+import { castDraft } from 'immer'
 
 /*
  * Test: Slice name is strongly typed.
@@ -552,3 +560,230 @@ const value = actionCreators.anyKey
   expectType<number>(nestedSelectors.selectMultiply(nestedState, 2))
   expectType<string>(nestedSelectors.selectToFixed(nestedState))
 }
+
+/**
+ * Test: reducer callback
+ */
+
+{
+  interface TestState {
+    foo: string
+  }
+
+  interface TestArg {
+    test: string
+  }
+
+  interface TestReturned {
+    payload: string
+  }
+
+  interface TestReject {
+    cause: string
+  }
+
+  const slice = createSlice({
+    name: 'test',
+    initialState: {} as TestState,
+    reducers: (create) => {
+      const pretypedAsyncThunk =
+        create.asyncThunk.withTypes<{ rejectValue: TestReject }>()
+
+      // @ts-expect-error
+      create.asyncThunk<any, any, { state: StoreState }>(() => {})
+
+      // @ts-expect-error
+      create.asyncThunk.withTypes<{
+        rejectValue: string
+        dispatch: StoreDispatch
+      }>()
+
+      return {
+        normalReducer: create.reducer<string>((state, action) => {
+          expectType<TestState>(state)
+          expectType<string>(action.payload)
+        }),
+        preparedReducer: create.preparedReducer(
+          (payload: string) => ({
+            payload,
+            meta: 'meta' as const,
+            error: 'error' as const,
+          }),
+          (state, action) => {
+            expectType<TestState>(state)
+            expectType<string>(action.payload)
+            expectExactType('meta' as const)(action.meta)
+            expectExactType('error' as const)(action.error)
+          }
+        ),
+        testInfer: create.asyncThunk(
+          function payloadCreator(arg: TestArg, api) {
+            return Promise.resolve<TestReturned>({ payload: 'foo' })
+          },
+          {
+            pending(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+            },
+            fulfilled(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+              expectType<TestReturned>(action.payload)
+            },
+            rejected(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+              expectType<SerializedError>(action.error)
+            },
+          }
+        ),
+        testExplicitType: create.asyncThunk<
+          TestArg,
+          TestReturned,
+          {
+            rejectValue: TestReject
+          }
+        >(
+          function payloadCreator(arg, api) {
+            // here would be a circular reference
+            expectUnknown(api.getState())
+            // here would be a circular reference
+            expectType<ThunkDispatch<any, any, any>>(api.dispatch)
+            // so you need to cast inside instead
+            const getState = api.getState as () => StoreState
+            const dispatch = api.dispatch as StoreDispatch
+            expectType<TestArg>(arg)
+            expectType<(value: TestReject) => any>(api.rejectWithValue)
+            return Promise.resolve({ payload: 'foo' })
+          },
+          {
+            pending(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+            },
+            fulfilled(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+              expectType<TestReturned>(action.payload)
+            },
+            rejected(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+              expectType<SerializedError>(action.error)
+              expectType<TestReject | undefined>(action.payload)
+            },
+          }
+        ),
+        testPretyped: pretypedAsyncThunk(
+          function payloadCreator(arg: TestArg, api) {
+            expectType<(value: TestReject) => any>(api.rejectWithValue)
+            return Promise.resolve<TestReturned>({ payload: 'foo' })
+          },
+          {
+            pending(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+            },
+            fulfilled(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+              expectType<TestReturned>(action.payload)
+            },
+            rejected(state, action) {
+              expectType<TestState>(state)
+              expectType<TestArg>(action.meta.arg)
+              expectType<SerializedError>(action.error)
+              expectType<TestReject | undefined>(action.payload)
+            },
+          }
+        ),
+      }
+    },
+  })
+
+  const store = configureStore({ reducer: { test: slice.reducer } })
+
+  type StoreState = ReturnType<typeof store.getState>
+  type StoreDispatch = typeof store.dispatch
+
+  expectType<PayloadActionCreator<string>>(slice.actions.normalReducer)
+  expectType<
+    ActionCreatorWithPreparedPayload<
+      [string],
+      string,
+      'test/preparedReducer',
+      'error',
+      'meta'
+    >
+  >(slice.actions.preparedReducer)
+  expectType<AsyncThunk<TestReturned, TestArg, {}>>(slice.actions.testInfer)
+  expectType<AsyncThunk<TestReturned, TestArg, { rejectValue: TestReject }>>(
+    slice.actions.testExplicitType
+  )
+  {
+    type TestInferThunk = AsyncThunk<TestReturned, TestArg, {}>
+    expectType<CaseReducer<TestState, ReturnType<TestInferThunk['pending']>>>(
+      slice.caseReducers.testInfer.pending
+    )
+    expectType<CaseReducer<TestState, ReturnType<TestInferThunk['fulfilled']>>>(
+      slice.caseReducers.testInfer.fulfilled
+    )
+    expectType<CaseReducer<TestState, ReturnType<TestInferThunk['rejected']>>>(
+      slice.caseReducers.testInfer.rejected
+    )
+  }
+}
+
+/** Test: wrapping createSlice should be possible, with callback */
+{
+  interface GenericState<T> {
+    data?: T
+    status: 'loading' | 'finished' | 'error'
+  }
+
+  const createGenericSlice = <
+    T,
+    Reducers extends SliceCaseReducers<GenericState<T>>
+  >({
+    name = '',
+    initialState,
+    reducers,
+  }: {
+    name: string
+    initialState: GenericState<T>
+    reducers: (create: ReducerCreators<GenericState<T>>) => Reducers
+  }) => {
+    return createSlice({
+      name,
+      initialState,
+      reducers: (create) => ({
+        start: create.reducer((state) => {
+          state.status = 'loading'
+        }),
+        success: create.reducer((state, action: PayloadAction<T>) => {
+          state.data = castDraft(action.payload)
+          state.status = 'finished'
+        }),
+        ...reducers(create),
+      }),
+    })
+  }
+
+  const wrappedSlice = createGenericSlice({
+    name: 'test',
+    initialState: { status: 'loading' } as GenericState<string>,
+    reducers: (create) => ({
+      magic: create.reducer((state) => {
+        expectType<GenericState<string>>(state)
+        // @ts-expect-error
+        expectType<GenericState<number>>(state)
+
+        state.status = 'finished'
+        state.data = 'hocus pocus'
+      }),
+    }),
+  })
+
+  expectType<ActionCreatorWithPayload<string>>(wrappedSlice.actions.success)
+  expectType<ActionCreatorWithoutPayload<string>>(wrappedSlice.actions.magic)
+}