Repository: reduxjs/redux-toolkit
Pull request #604 (2020-06-12 04:21:50)

Bump immer to v7, export current util, add usage docs
- [x] Review comment about adding an `isDraftable` check inside `createReducer` from Discord

  > and mweststrate pinged me about adding an isDraftable check to createReducer.

- [x] Figure out how to best warn about a non-draftable state

- [x] Review selector usage inside of a reducer and references to 'leaking outside of the draft' - https://github.com/immerjs/immer/issues/561, would need reproduction in a test

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

Built with commit 8410e898526b17fc499dd9b26f3618c1637c2eda

https://deploy-preview-604--redux-starter-kit-docs.netlify.app
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/604/builds/32070) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit da59d40d0aeb0870f936b7abb8fc2db0c4e2787e:

|Sandbox| Source |
|--|--|
|[gallant-firefly-p8qx6](https://codesandbox.io/s/gallant-firefly-p8qx6)| Configuration |
|[bold-haze-hezgc](https://codesandbox.io/s/bold-haze-hezgc)| Configuration |
|[throbbing-star-lo02t](https://codesandbox.io/s/throbbing-star-lo02t)| Configuration |

msutkowski: This should be good to go. Adding a few additional tests that explicitly expect edge cases on return values. Pending an update here: https://github.com/immerjs/immer/pull/617

Patch:
diff --git a/docs/api/createAsyncThunk.md b/docs/api/createAsyncThunk.md
index 42e72243..08a8202f 100644
--- a/docs/api/createAsyncThunk.md
+++ b/docs/api/createAsyncThunk.md
@@ -66,7 +66,7 @@ For example, a `type` argument of `'users/requestStatus'` will generate these ac
 
 ### `payloadCreator`
 
-A callback function that should return a promise containing the result of some asynchronous logic. It may also return a value synchronously. If there is an error, it should either return a rejected promise containing an `Error` instance or a plain value such as a descriptive error message or otherwise a resolved promise with a `RejectWithValue` argument as returned by the `thunkApi.rejectWithValue` function.
+A callback function that should return a promise containing the result of some asynchronous logic. It may also return a value synchronously. If there is an error, it should either return a rejected promise containing an `Error` instance or a plain value such as a descriptive error message or otherwise a resolved promise with a `RejectWithValue` argument as returned by the `thunkAPI.rejectWithValue` function.
 
 The `payloadCreator` function can contain whatever logic you need to calculate an appropriate result. This could include a standard AJAX data fetch request, multiple AJAX calls with the results combined into a final value, interactions with React Native `AsyncStorage`, and so on.
 
@@ -295,7 +295,7 @@ function MyComponent(props: { userId: string }) {
 
 After a thunk has been cancelled this way, it will dispatch (and return) a `"thunkName/rejected"` action with an `AbortError` on the `error` property. The thunk will not dispatch any further actions.
 
-Additionally, your `payloadCreator` can use the `AbortSignal` it is passed via `thunkApi.signal` to actually cancel a costly asynchronous action.
+Additionally, your `payloadCreator` can use the `AbortSignal` it is passed via `thunkAPI.signal` to actually cancel a costly asynchronous action.
 
 The `fetch` api of modern browsers already comes with support for an `AbortSignal`:
 
@@ -481,7 +481,7 @@ const usersSlice = createSlice({
 })
 
 const UsersComponent = () => {
-  const { users, loading, error } = useSelector(state => state.users)
+  const { entities, error } = useSelector(state => state.users)
   const dispatch = useDispatch()
 
   // This is an example of an onSubmit handler using Formik meant to demonstrate accessing the payload of the rejected action
@@ -582,9 +582,7 @@ const usersSlice = createSlice({
 })
 
 const UsersComponent = () => {
-  const { users, loading, error } = useSelector(
-    (state: RootState) => state.users
-  )
+  const { entities, error } = useSelector((state: RootState) => state.users)
   const dispatch: AppDispatch = useDispatch()
 
   // This is an example of an onSubmit handler using Formik meant to demonstrate accessing the payload of the rejected action
diff --git a/docs/api/createReducer.md b/docs/api/createReducer.md
index b5024a06..caf93b05 100644
--- a/docs/api/createReducer.md
+++ b/docs/api/createReducer.md
@@ -135,3 +135,26 @@ const todosReducer = createReducer([], {
   }
 })
 ```
+
+## Debugging your state
+
+It's very common for a developer to call `console.log(state)` during the development process. However, browsers display Proxies in a format that is hard to read, which can make console logging of Immer-based state difficult.
+
+When using either `createSlice` or `createReducer`, you may use the [`current`](./otherExports#current.md) utility that we re-export from the [`immer` library](https://immerjs.github.io/immer). This utility creates a separate plain copy of the current Immer `Draft` state value, which can then be logged for viewing as normal.
+
+```ts
+// todosSlice.js
+import { createSlice, current } from '@reduxjs/toolkit'
+
+const slice = createSlice({
+  name: 'todos',
+  initialState: [{ id: 1, title: 'Example todo' }],
+  reducers: {
+    addTodo: (state, action) => {
+      console.log('before', current(state))
+      state.push(action.payload)
+      console.log('after', current(state))
+    }
+  }
+})
+```
diff --git a/docs/api/getDefaultMiddleware.md b/docs/api/getDefaultMiddleware.md
index bc9928a3..d5978285 100644
--- a/docs/api/getDefaultMiddleware.md
+++ b/docs/api/getDefaultMiddleware.md
@@ -41,12 +41,29 @@ middleware added as well:
 ```js
 const store = configureStore({
   reducer: rootReducer,
-  middleware: [...getDefaultMiddleware(), logger]
+  middleware: getDefaultMiddleware().concat(logger)
 })
 
 // Store has all of the default middleware added, _plus_ the logger middleware
 ```
 
+## Usage without import (middleware callback notation)
+
+For convenience, the `middleware` property of `configureStore` can be used with a callback notation like this.
+
+```js
+const store = configureStore({
+  reducer: rootReducer,
+  middleware: getDefaultMiddleware => getDefaultMiddleware().concat(logger)
+})
+
+// Store has all of the default middleware added, _plus_ the logger middleware
+```
+
+While this does not make much of a difference for JavaScript users, using this notation is preferrable to TypeScript users, as that version of `getDefaultMiddleware` is already correctly pre-typed for the `Store`'s type, so no use of generics is necessary.
+
+Also, when using TypeScript, it is preferrable to use the chainable `.concat(...)` and `.prepend(...)` methods of the returned `MiddlewareArray` instead of the array spread operator, as the latter can lose valuable type information under some circumstances.
+
 ## Included Default Middleware
 
 ### Development
diff --git a/docs/api/otherExports.md b/docs/api/otherExports.md
index 8ef68663..49e07b4c 100644
--- a/docs/api/otherExports.md
+++ b/docs/api/otherExports.md
@@ -156,6 +156,23 @@ console.log(nanoid())
 
 The default immutable update function from the [`immer` library](https://immerjs.github.io/immer/), re-exported here as `createNextState` (also commonly referred to as [`produce`](https://immerjs.github.io/immer/docs/produce))
 
+### `current`
+
+[The `current` function](https://immerjs.github.io/immer/docs/current) from the [`immer` library](https://immerjs.github.io/immer/), which takes a snapshot of the current state of a draft and finalizes it (but without freezing). Current is a great utility to print the current state during debugging, and the output of `current` can also be safely leaked outside the producer.
+
+```js
+import { createReducer, createAction, current } from '@reduxjs/toolkit'
+
+const addTodo = createAction('addTodo')
+
+const todosReducer = createReducer([], {
+  [addTodo]: (state, action) => {
+    state.push(action.payload)
+    console.log(current(state))
+  }
+})
+```
+
 ### `combineReducers`
 
 Redux's [`combineReducers`](https://redux.js.org/api/combinereducers), re-exported for convenience. While `configureStore` calls this internally, you may wish to call it yourself to compose multiple levels of slice reducers.
diff --git a/docs/usage/usage-with-typescript.md b/docs/usage/usage-with-typescript.md
index c076b871..b3320486 100644
--- a/docs/usage/usage-with-typescript.md
+++ b/docs/usage/usage-with-typescript.md
@@ -62,13 +62,14 @@ export const useAppDispatch = () => useDispatch<AppDispatch>() // Export a hook
 
 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.
 
-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.
+As TypeScript often widens array types when combining arrays using the spread operator, we suggest using the `.concat(...)` and `.prepend(...)` methods of the `MiddlewareArray` returned by `getDefaultMiddleware()`.
 
-Please note that when calling `getDefaultMiddleware` in TypeScript, you have to provide the state type as a generic argument.
+Also, we suggest using the callback notation for the `middleware` option go get a correctly pre-typed version of `getDefaultMiddleware` that does not require you to specify any generics by hand.
 
 ```ts {10-20}
 import { configureStore } from '@reduxjs/toolkit'
 import additionalMiddleware from 'additional-middleware'
+import logger from 'redux-logger'
 // @ts-ignore
 import untypedMiddleware from 'untyped-middleware'
 import rootReducer from './rootReducer'
@@ -76,23 +77,45 @@ 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>`
+  middleware: getDefaultMiddleware =>
+    getDefaultMiddleware()
+      .prepend(
+        // correctly typed middlewares can just be used
+        additionalMiddleware,
+        // you can also manually type middlewares manually
+        untypedMiddleware as Middleware<
+          (action: Action<'specialAction'>) => number,
+          RootState
+        >
+      )
+      // prepend and concat calls can be chained
+      .concat(logger)
 })
 
 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 `MiddlewareArray` without `getDefaultMiddleware`
+
+If you want to skip the usage of `getDefaultMiddleware` altogether, you can still use `MiddlewareArray` for type-safe concatenation of your `middleware` array. This class extends the default JavaScript `Array` type, only with modified typings for `.concat(...)` and the additional `.prepend(...)` method.
+
+This is generally not required though, as you will probably not run into any array-type-widening issues as long as you are using `as const` and do not use the spread operator.
+
+So the following two calls would be equivalent:
+
+```ts
+import { configureStore, MiddlewareArray } from '@reduxjs/toolkit'
+
+configureStore({
+  reducer: rootReducer,
+  middleware: new MiddlewareArray().concat(additionalMiddleware, logger)
+})
+
+configureStore({
+  reducer: rootReducer,
+  middleware: [additionalMiddleware, logger] as const
+})
+```
 
 ### Using the extracted `Dispatch` type with React-Redux
 
diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md
index 401db656..7c91aca3 100644
--- a/etc/redux-toolkit.api.md
+++ b/etc/redux-toolkit.api.md
@@ -9,6 +9,7 @@ import { ActionCreator } from 'redux';
 import { AnyAction } from 'redux';
 import { default as createNextState } from 'immer';
 import { createSelector } from 'reselect';
+import { current } from 'immer';
 import { DeepPartial } from 'redux';
 import { Dispatch } from 'redux';
 import { Draft } from 'immer';
@@ -59,6 +60,14 @@ export interface ActionReducerMapBuilder<State> {
 // @public @deprecated
 export type Actions<T extends keyof any = string> = Record<T, Action>;
 
+// @public
+export type AsyncThunk<Returned, ThunkArg, ThunkApiConfig extends AsyncThunkConfig> = AsyncThunkActionCreator<Returned, ThunkArg, ThunkApiConfig> & {
+    pending: AsyncThunkPendingActionCreator<ThunkArg>;
+    rejected: AsyncThunkRejectedActionCreator<ThunkArg, ThunkApiConfig>;
+    fulfilled: AsyncThunkFulfilledActionCreator<Returned, ThunkArg>;
+    typePrefix: string;
+};
+
 // @public
 export type AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig extends AsyncThunkConfig> = (dispatch: GetDispatch<ThunkApiConfig>, getState: () => GetState<ThunkApiConfig>, extra: GetExtra<ThunkApiConfig>) => Promise<PayloadAction<Returned, string, {
     arg: ThunkArg;
@@ -112,7 +121,7 @@ export function configureStore<S = any, A extends Action = AnyAction, M extends
 export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction, M extends Middlewares<S> = Middlewares<S>> {
     devTools?: boolean | EnhancerOptions;
     enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback;
-    middleware?: M;
+    middleware?: ((getDefaultMiddleware: CurriedGetDefaultMiddleware<S>) => M) | M;
     preloadedState?: DeepPartial<S extends any ? S : S>;
     reducer: Reducer<S, A> | ReducersMapObject<S, A>;
 }
@@ -124,23 +133,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>;
 
 // @public (undocumented)
-export function createAsyncThunk<Returned, ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {}>(typePrefix: string, payloadCreator: (arg: ThunkArg, thunkAPI: GetThunkAPI<ThunkApiConfig>) => Promise<Returned | RejectWithValue<GetRejectValue<ThunkApiConfig>>> | Returned | RejectWithValue<GetRejectValue<ThunkApiConfig>>, options?: AsyncThunkOptions<ThunkArg, ThunkApiConfig>): IsAny<ThunkArg, (arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>, unknown extends ThunkArg ? (arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> : [ThunkArg] extends [void] | [undefined] ? () => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> : [void] extends [ThunkArg] ? (arg?: ThunkArg | undefined) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> : [undefined] extends [ThunkArg] ? (arg?: ThunkArg | undefined) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> : (arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>> & {
-    pending: ActionCreatorWithPreparedPayload<[string, ThunkArg], undefined, string, never, {
-        arg: ThunkArg;
-        requestId: string;
-    }>;
-    rejected: ActionCreatorWithPreparedPayload<[Error | null, string, ThunkArg, (GetRejectValue<ThunkApiConfig> | undefined)?], GetRejectValue<ThunkApiConfig> | undefined, string, SerializedError, {
-        arg: ThunkArg;
-        requestId: string;
-        aborted: boolean;
-        condition: boolean;
-    }>;
-    fulfilled: ActionCreatorWithPreparedPayload<[Returned, string, ThunkArg], Returned, string, never, {
-        arg: ThunkArg;
-        requestId: string;
-    }>;
-    typePrefix: string;
-};
+export function createAsyncThunk<Returned, ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {}>(typePrefix: string, payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>, options?: AsyncThunkOptions<ThunkArg, ThunkApiConfig>): AsyncThunk<Returned, ThunkArg, ThunkApiConfig>;
 
 // @public (undocumented)
 export function createEntityAdapter<T>(options?: {
@@ -175,6 +168,8 @@ export interface CreateSliceOptions<State = any, CR extends SliceCaseReducers<St
     reducers: ValidateSliceCaseReducers<State, CR>;
 }
 
+export { current }
+
 // @public (undocumented)
 export interface Dictionary<T> extends DictionaryNum<T> {
     // (undocumented)
@@ -279,7 +274,7 @@ export function getDefaultMiddleware<S = any, O extends Partial<GetDefaultMiddle
     thunk: true;
     immutableCheck: true;
     serializableCheck: true;
-}>(options?: O): Array<Middleware<{}, S> | ThunkMiddlewareFor<S, O>>;
+}>(options?: O): MiddlewareArray<Middleware<{}, S> | ThunkMiddlewareFor<S, O>>;
 
 // @public
 export function getType<T extends string>(actionCreator: PayloadActionCreator<any, T>): T;
@@ -305,6 +300,18 @@ export function isImmutableDefault(value: unknown): boolean;
 // @public
 export function isPlain(val: any): boolean;
 
+// @public (undocumented)
+export class MiddlewareArray<Middlewares extends Middleware<any, any>> extends Array<Middlewares> {
+    // (undocumented)
+    concat<AdditionalMiddlewares extends ReadonlyArray<Middleware<any, any>>>(items: AdditionalMiddlewares): MiddlewareArray<Middlewares | AdditionalMiddlewares[number]>;
+    // (undocumented)
+    concat<AdditionalMiddlewares extends ReadonlyArray<Middleware<any, any>>>(...items: AdditionalMiddlewares): MiddlewareArray<Middlewares | AdditionalMiddlewares[number]>;
+    // (undocumented)
+    prepend<AdditionalMiddlewares extends ReadonlyArray<Middleware<any, any>>>(items: AdditionalMiddlewares): MiddlewareArray<AdditionalMiddlewares[number] | Middlewares>;
+    // (undocumented)
+    prepend<AdditionalMiddlewares extends ReadonlyArray<Middleware<any, any>>>(...items: AdditionalMiddlewares): MiddlewareArray<AdditionalMiddlewares[number] | Middlewares>;
+}
+
 // @public (undocumented)
 export let nanoid: (size?: number) => string;
 
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 00000000..5a67ec3c
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+  setupFilesAfterEnv: ['./jest.setup.js']
+}
diff --git a/jest.setup.js b/jest.setup.js
new file mode 100644
index 00000000..ed3889f8
--- /dev/null
+++ b/jest.setup.js
@@ -0,0 +1,4 @@
+process.on('unhandledRejection', error => {
+  // eslint-disable-next-line no-undef
+  fail(error)
+})
diff --git a/package-lock.json b/package-lock.json
index 30ac45d3..e5eeac51 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4563,9 +4563,9 @@
       "dev": true
     },
     "immer": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/immer/-/immer-6.0.1.tgz",
-      "integrity": "sha512-oXwigCKgznQywsXi1VgrqgWbQEU3wievNCVc4Fcwky6mwXU6YHj6JuYp0WEM/B1EphkqsLr0x18lm5OiuemPcA=="
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.1.tgz",
+      "integrity": "sha512-DpWL/ES2Ba8KwW+A5AgNRoJPZP8LxdHejKXar1VfbF5a7ATvvekzmmNSQxje+WG0DIhuanvFEumFffVqyCLmBQ=="
     },
     "import-fresh": {
       "version": "3.2.1",
diff --git a/package.json b/package.json
index 67770aa2..a6422b21 100644
--- a/package.json
+++ b/package.json
@@ -58,7 +58,7 @@
     "src"
   ],
   "dependencies": {
-    "immer": "^6.0.1",
+    "immer": "7.0.1",
     "redux": "^4.0.0",
     "redux-thunk": "^2.3.0",
     "reselect": "^4.0.0"
diff --git a/src/configureStore.ts b/src/configureStore.ts
index 907641be..14637f67 100644
--- a/src/configureStore.ts
+++ b/src/configureStore.ts
@@ -20,8 +20,9 @@ import {
 
 import isPlainObject from './isPlainObject'
 import {
-  getDefaultMiddleware,
-  ThunkMiddlewareFor
+  ThunkMiddlewareFor,
+  curryGetDefaultMiddleware,
+  CurriedGetDefaultMiddleware
 } from './getDefaultMiddleware'
 import { DispatchForMiddlewares } from './tsHelpers'
 
@@ -56,7 +57,7 @@ export interface ConfigureStoreOptions<
    * An array of Redux middleware to install. If not supplied, defaults to
    * the set of middleware returned by `getDefaultMiddleware()`.
    */
-  middleware?: M
+  middleware?: ((getDefaultMiddleware: CurriedGetDefaultMiddleware<S>) => M) | M
 
   /**
    * Whether to enable Redux DevTools integration. Defaults to `true`.
@@ -124,9 +125,11 @@ export function configureStore<
   A extends Action = AnyAction,
   M extends Middlewares<S> = [ThunkMiddlewareFor<S>]
 >(options: ConfigureStoreOptions<S, A, M>): EnhancedStore<S, A, M> {
+  const curriedGetDefaultMiddleware = curryGetDefaultMiddleware<S>()
+
   const {
     reducer = undefined,
-    middleware = getDefaultMiddleware(),
+    middleware = curriedGetDefaultMiddleware(),
     devTools = true,
     preloadedState = undefined,
     enhancers = undefined
@@ -144,7 +147,11 @@ export function configureStore<
     )
   }
 
-  const middlewareEnhancer = applyMiddleware(...middleware)
+  const middlewareEnhancer = applyMiddleware(
+    ...(typeof middleware === 'function'
+      ? middleware(curriedGetDefaultMiddleware)
+      : middleware)
+  )
 
   let finalCompose = compose
 
diff --git a/src/createAsyncThunk.ts b/src/createAsyncThunk.ts
index d217dff4..cab1d2c8 100644
--- a/src/createAsyncThunk.ts
+++ b/src/createAsyncThunk.ts
@@ -215,6 +215,71 @@ interface AsyncThunkOptions<
   dispatchConditionRejection?: boolean
 }
 
+type AsyncThunkPendingActionCreator<
+  ThunkArg
+> = ActionCreatorWithPreparedPayload<
+  [string, ThunkArg],
+  undefined,
+  string,
+  never,
+  {
+    arg: ThunkArg
+    requestId: string
+  }
+>
+
+type AsyncThunkRejectedActionCreator<
+  ThunkArg,
+  ThunkApiConfig
+> = ActionCreatorWithPreparedPayload<
+  [
+    Error | null,
+    string,
+    ThunkArg,
+    (GetRejectValue<ThunkApiConfig> | undefined)?
+  ],
+  GetRejectValue<ThunkApiConfig> | undefined,
+  string,
+  SerializedError,
+  {
+    arg: ThunkArg
+    requestId: string
+    aborted: boolean
+    condition: boolean
+  }
+>
+
+type AsyncThunkFulfilledActionCreator<
+  Returned,
+  ThunkArg
+> = ActionCreatorWithPreparedPayload<
+  [Returned, string, ThunkArg],
+  Returned,
+  string,
+  never,
+  {
+    arg: ThunkArg
+    requestId: string
+  }
+>
+
+/**
+ * A type describing the return value of `createAsyncThunk`.
+ * Might be useful for wrapping `createAsyncThunk` in custom abstractions.
+ *
+ * @public
+ */
+export type AsyncThunk<
+  Returned,
+  ThunkArg,
+  ThunkApiConfig extends AsyncThunkConfig
+> = AsyncThunkActionCreator<Returned, ThunkArg, ThunkApiConfig> & {
+  pending: AsyncThunkPendingActionCreator<ThunkArg>
+  rejected: AsyncThunkRejectedActionCreator<ThunkArg, ThunkApiConfig>
+  fulfilled: AsyncThunkFulfilledActionCreator<Returned, ThunkArg>
+  typePrefix: string
+}
+
 /**
  *
  * @param typePrefix
@@ -229,15 +294,9 @@ export function createAsyncThunk<
   ThunkApiConfig extends AsyncThunkConfig = {}
 >(
   typePrefix: string,
-  payloadCreator: (
-    arg: ThunkArg,
-    thunkAPI: GetThunkAPI<ThunkApiConfig>
-  ) =>
-    | Promise<Returned | RejectWithValue<GetRejectValue<ThunkApiConfig>>>
-    | Returned
-    | RejectWithValue<GetRejectValue<ThunkApiConfig>>,
+  payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>,
   options?: AsyncThunkOptions<ThunkArg, ThunkApiConfig>
-) {
+): AsyncThunk<Returned, ThunkArg, ThunkApiConfig> {
   type RejectedValue = GetRejectValue<ThunkApiConfig>
 
   const fulfilled = createAction(
@@ -326,9 +385,12 @@ If you want to use the AbortController to react to \`abort\` events, please cons
         )
       )
 
+      let started = false
       function abort(reason?: string) {
-        abortReason = reason
-        abortController.abort()
+        if (started) {
+          abortReason = reason
+          abortController.abort()
+        }
       }
 
       const promise = (async function() {
@@ -339,11 +401,13 @@ If you want to use the AbortController to react to \`abort\` events, please cons
             options.condition &&
             options.condition(arg, { getState, extra }) === false
           ) {
+            // eslint-disable-next-line no-throw-literal
             throw {
               name: 'ConditionError',
               message: 'Aborted due to condition callback returning false.'
             }
           }
+          started = true
           dispatch(pending(requestId, arg))
           finalAction = await Promise.race([
             abortedPromise,
diff --git a/src/createReducer.ts b/src/createReducer.ts
index 9894fcdb..c516a6c6 100644
--- a/src/createReducer.ts
+++ b/src/createReducer.ts
@@ -1,4 +1,4 @@
-import createNextState, { Draft, isDraft } from 'immer'
+import createNextState, { Draft, isDraft, isDraftable } from 'immer'
 import { AnyAction, Action, Reducer } from 'redux'
 import {
   executeReducerBuilderCallback,
@@ -108,11 +108,29 @@ export function createReducer<S>(
     const caseReducer = actionsMap[action.type]
     if (caseReducer) {
       if (isDraft(state)) {
-        // we must already be inside a `createNextState` call, likely because
-        // this is being wrapped in `createReducer`, `createSlice`, or nested
+        // If it's already a draft, we must already be inside a `createNextState` call,
+        // likely because this is being wrapped in `createReducer`, `createSlice`, or nested
         // inside an existing draft. It's safe to just pass the draft to the mutator.
-        const draft = state as Draft<S> // We can aassume this is already a draft
-        return caseReducer(draft, action) || state
+        const draft = state as Draft<S> // We can assume this is already a draft
+        const result = caseReducer(draft, action)
+
+        if (typeof result === 'undefined') {
+          return state
+        }
+
+        return result
+      } else if (!isDraftable(state)) {
+        // If state is not draftable (ex: a primitive, such as 0), we want to directly
+        // return the caseReducer func and not wrap it with produce.
+        const result = caseReducer(state as any, action)
+
+        if (typeof result === 'undefined') {
+          throw Error(
+            'A case reducer on a non-draftable value must not return undefined'
+          )
+        }
+
+        return result
       } else {
         // @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
         // than an Immutable<S>, and TypeScript cannot find out how to reconcile
diff --git a/src/getDefaultMiddleware.ts b/src/getDefaultMiddleware.ts
index 5efcf717..e5f57085 100644
--- a/src/getDefaultMiddleware.ts
+++ b/src/getDefaultMiddleware.ts
@@ -11,6 +11,7 @@ import {
   createSerializableStateInvariantMiddleware,
   SerializableStateInvariantMiddlewareOptions
 } from './serializableStateInvariantMiddleware'
+import { MiddlewareArray } from './utils'
 
 function isBoolean(x: any): x is boolean {
   return typeof x === 'boolean'
@@ -39,6 +40,24 @@ export type ThunkMiddlewareFor<
       | ThunkMiddleware<S, AnyAction, null> //The ThunkMiddleware with a `null` ExtraArgument is here to provide backwards-compatibility.
       | ThunkMiddleware<S, AnyAction>
 
+export type CurriedGetDefaultMiddleware<S = any> = <
+  O extends Partial<GetDefaultMiddlewareOptions> = {
+    thunk: true
+    immutableCheck: true
+    serializableCheck: true
+  }
+>(
+  options?: O
+) => MiddlewareArray<Middleware<{}, S> | ThunkMiddlewareFor<S, O>>
+
+export function curryGetDefaultMiddleware<
+  S = any
+>(): CurriedGetDefaultMiddleware<S> {
+  return function curriedGetDefaultMiddleware(options) {
+    return getDefaultMiddleware(options)
+  }
+}
+
 /**
  * Returns any array containing the default middleware installed by
  * `configureStore()`. Useful if you want to configure your store with a custom
@@ -55,14 +74,16 @@ export function getDefaultMiddleware<
     immutableCheck: true
     serializableCheck: true
   }
->(options: O = {} as O): Array<Middleware<{}, S> | ThunkMiddlewareFor<S, O>> {
+>(
+  options: O = {} as O
+): MiddlewareArray<Middleware<{}, S> | ThunkMiddlewareFor<S, O>> {
   const {
     thunk = true,
     immutableCheck = true,
     serializableCheck = true
   } = options
 
-  let middlewareArray: Middleware<{}, S>[] = []
+  let middlewareArray: Middleware<{}, S>[] = new MiddlewareArray()
 
   if (thunk) {
     if (isBoolean(thunk)) {
diff --git a/src/index.ts b/src/index.ts
index f4a1843d..d33fc5f5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,6 @@
 import { enableES5 } from 'immer'
 export * from 'redux'
-export { default as createNextState, Draft } from 'immer'
+export { default as createNextState, Draft, current } from 'immer'
 export {
   createSelector,
   Selector,
@@ -81,6 +81,7 @@ export {
   // types
   ActionReducerMapBuilder
 } from './mapBuilders'
+export { MiddlewareArray } from './utils'
 
 export { createEntityAdapter } from './entities/create_adapter'
 export {
@@ -96,6 +97,7 @@ export {
 } from './entities/models'
 
 export {
+  AsyncThunk,
   AsyncThunkAction,
   AsyncThunkPayloadCreatorReturnValue,
   AsyncThunkPayloadCreator,
diff --git a/src/utils.ts b/src/utils.ts
index 933c7f4a..5d9d4205 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,3 +1,5 @@
+import { Middleware } from 'redux'
+
 export function getTimeMeasureUtils(maxDelay: number, fnName: string) {
   let elapsed = 0
   return {
@@ -19,3 +21,37 @@ It is disabled in production builds, so you don't need to worry about that.`)
     }
   }
 }
+
+/**
+ * @public
+ */
+export class MiddlewareArray<
+  Middlewares extends Middleware<any, any>
+> extends Array<Middlewares> {
+  concat<AdditionalMiddlewares extends ReadonlyArray<Middleware<any, any>>>(
+    items: AdditionalMiddlewares
+  ): MiddlewareArray<Middlewares | AdditionalMiddlewares[number]>
+
+  concat<AdditionalMiddlewares extends ReadonlyArray<Middleware<any, any>>>(
+    ...items: AdditionalMiddlewares
+  ): MiddlewareArray<Middlewares | AdditionalMiddlewares[number]>
+
+  concat(...arr: any[]) {
+    return new MiddlewareArray(...super.concat(...arr))
+  }
+
+  prepend<AdditionalMiddlewares extends ReadonlyArray<Middleware<any, any>>>(
+    items: AdditionalMiddlewares
+  ): MiddlewareArray<AdditionalMiddlewares[number] | Middlewares>
+
+  prepend<AdditionalMiddlewares extends ReadonlyArray<Middleware<any, any>>>(
+    ...items: AdditionalMiddlewares
+  ): MiddlewareArray<AdditionalMiddlewares[number] | Middlewares>
+
+  prepend(...arr: any[]) {
+    if (arr.length === 1 && Array.isArray(arr[0])) {
+      return new MiddlewareArray(...arr[0].concat(this))
+    }
+    return new MiddlewareArray(...arr.concat(this))
+  }
+}
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
index c0ee0de1..570dbc67 100644
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -27,7 +27,6 @@ module.exports = {
     repoUrl: 'https://github.com/reduxjs/redux-toolkit'
   },
   themeConfig: {
-    disableDarkMode: true,
     prism: {
       theme: require('./src/js/monokaiTheme.js')
     },
diff --git a/website/src/css/custom.css b/website/src/css/custom.css
index b0cce427..82258b8f 100644
--- a/website/src/css/custom.css
+++ b/website/src/css/custom.css
@@ -20,7 +20,7 @@
 
   --ifm-blockquote-color: #ecf4f9;
   --ifm-blockquote-color-dark: #cbddea;
-  
+
   --ifm-code-padding-vertical: 0.1rem;
   --ifm-code-padding-horizontal: 0.2rem;
 
@@ -74,11 +74,23 @@ code {
   border-radius: 0.2rem;
 }
 
+html[data-theme='dark'] code {
+  background-color: var(--ifm-color-emphasis-200);
+  border-radius: 0.2rem;
+}
+
 a code,
 code a {
+  background-color: var(--ifm-color-emphasis-200);
   color: inherit;
 }
 
+html[data-theme='dark'] a code,
+html[data-theme='dark'] code a {
+  background-color: var(--ifm-color-emphasis-100);
+  color: var(--ifm-color-primary-lighter);
+}
+
 a.contents__link > code {
   color: inherit;
 }
diff --git a/website/src/pages/index.js b/website/src/pages/index.js
index 00626078..6fff0b6b 100644
--- a/website/src/pages/index.js
+++ b/website/src/pages/index.js
@@ -110,7 +110,7 @@ const otherLibraries = [
     image: (
       <svg
         xmlns="http://www.w3.org/2000/svg"
-        ariaHidden="true"
+        aria-hidden="true"
         data-icon="external-link-square-alt"
         data-prefix="fas"
         viewBox="0 0 448 512"


Test patch:
diff --git a/src/configureStore.test.ts b/src/configureStore.test.ts
index a7a4bab6..b8f1f726 100644
--- a/src/configureStore.test.ts
+++ b/src/configureStore.test.ts
@@ -84,6 +84,26 @@ describe('configureStore', () => {
     })
   })
 
+  describe('middleware builder notation', () => {
+    it('calls builder, passes getDefaultMiddleware and uses returned middlewares', () => {
+      const thank = jest.fn((_store => next => action =>
+        'foobar') as redux.Middleware)
+
+      const builder = jest.fn(getDefaultMiddleware => {
+        expect(getDefaultMiddleware).toEqual(expect.any(Function))
+        expect(getDefaultMiddleware()).toEqual(expect.any(Array))
+
+        return [thank]
+      })
+
+      const store = configureStore({ middleware: builder, reducer })
+
+      expect(builder).toHaveBeenCalled()
+
+      expect(store.dispatch({ type: 'test' })).toBe('foobar')
+    })
+  })
+
   describe('with devTools disabled', () => {
     it('calls createStore without devTools enhancer', () => {
       expect(configureStore({ devTools: false, reducer })).toBeInstanceOf(
diff --git a/src/createAsyncThunk.test.ts b/src/createAsyncThunk.test.ts
index 49969318..8e52f864 100644
--- a/src/createAsyncThunk.test.ts
+++ b/src/createAsyncThunk.test.ts
@@ -549,6 +549,21 @@ describe('conditional skipping of asyncThunks', () => {
     expect(dispatch).toHaveBeenCalledTimes(0)
   })
 
+  test('does not fail when attempting to abort a canceled promise', async () => {
+    const asyncPayloadCreator = jest.fn(async (x: typeof arg) => {
+      await new Promise(resolve => setTimeout(resolve, 2000))
+      return 10
+    })
+
+    const asyncThunk = createAsyncThunk('test', asyncPayloadCreator, {
+      condition
+    })
+    const promise = asyncThunk(arg)(dispatch, getState, extra)
+    promise.abort(
+      `If the promise was 1. somehow canceled, 2. in a 'started' state and 3. we attempted to abort, this would crash the tests`
+    )
+  })
+
   test('rejected action can be dispatched via option', async () => {
     const asyncThunk = createAsyncThunk('test', payloadCreator, {
       condition,
diff --git a/src/createReducer.test.ts b/src/createReducer.test.ts
index 953c0c25..ba69815a 100644
--- a/src/createReducer.test.ts
+++ b/src/createReducer.test.ts
@@ -149,6 +149,38 @@ describe('createReducer', () => {
       expect(reducer(0, increment(5))).toBe(5)
       expect(reducer(5, decrement(5))).toBe(0)
     })
+    test('will throw an error when returning undefined from a non-draftable state', () => {
+      const reducer = createReducer(0, builder =>
+        builder.addCase(
+          'decrement',
+          (state, action: { type: 'decrement'; payload: number }) => {}
+        )
+      )
+      expect(() => reducer(5, decrement(5))).toThrowErrorMatchingInlineSnapshot(
+        `"A case reducer on a non-draftable value must not return undefined"`
+      )
+    })
+    test('allows you to return null', () => {
+      const reducer = createReducer(0 as number | null, builder =>
+        builder.addCase(
+          'decrement',
+          (state, action: { type: 'decrement'; payload: number }) => {
+            return null
+          }
+        )
+      )
+      expect(reducer(5, decrement(5))).toBe(null)
+    })
+    test('allows you to return 0', () => {
+      const reducer = createReducer(0, builder =>
+        builder.addCase(
+          'decrement',
+          (state, action: { type: 'decrement'; payload: number }) =>
+            state - action.payload
+        )
+      )
+      expect(reducer(5, decrement(5))).toBe(0)
+    })
     test('will throw if the same type is used twice', () => {
       expect(() =>
         createReducer(0, builder =>
diff --git a/src/createSlice.test.ts b/src/createSlice.test.ts
index 81092b7b..b1927854 100644
--- a/src/createSlice.test.ts
+++ b/src/createSlice.test.ts
@@ -181,7 +181,7 @@ describe('createSlice', () => {
     })
 
     it('should call the reducer function', () => {
-      const reducer = jest.fn()
+      const reducer = jest.fn(() => 5)
 
       const testSlice = createSlice({
         name: 'test',
diff --git a/src/getDefaultMiddleware.test.ts b/src/getDefaultMiddleware.test.ts
index 6791d191..82ccfc31 100644
--- a/src/getDefaultMiddleware.test.ts
+++ b/src/getDefaultMiddleware.test.ts
@@ -1,6 +1,5 @@
-import { AnyAction } from 'redux'
-import { getDefaultMiddleware } from './getDefaultMiddleware'
-import { configureStore } from './configureStore'
+import { AnyAction, Middleware } from 'redux'
+import { getDefaultMiddleware, MiddlewareArray, configureStore } from '.'
 import thunk, { ThunkAction } from 'redux-thunk'
 
 describe('getDefaultMiddleware', () => {
@@ -116,3 +115,120 @@ describe('getDefaultMiddleware', () => {
     expect(serializableCheckWasCalled).toBe(true)
   })
 })
+
+describe('MiddlewareArray functionality', () => {
+  const middleware1: Middleware = () => next => action => next(action)
+  const middleware2: Middleware = () => next => action => next(action)
+  const defaultMiddleware = getDefaultMiddleware()
+  const originalDefaultMiddleware = [...defaultMiddleware]
+
+  test('allows to prepend a single value', () => {
+    const prepended = defaultMiddleware.prepend(middleware1)
+
+    // value is prepended
+    expect(prepended).toEqual([middleware1, ...defaultMiddleware])
+    // returned value is of correct type
+    expect(prepended).toBeInstanceOf(MiddlewareArray)
+    // prepended is a new array
+    expect(prepended).not.toEqual(defaultMiddleware)
+    // defaultMiddleware is not modified
+    expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
+  })
+
+  test('allows to prepend multiple values (array as first argument)', () => {
+    const prepended = defaultMiddleware.prepend([middleware1, middleware2])
+
+    // value is prepended
+    expect(prepended).toEqual([middleware1, middleware2, ...defaultMiddleware])
+    // returned value is of correct type
+    expect(prepended).toBeInstanceOf(MiddlewareArray)
+    // prepended is a new array
+    expect(prepended).not.toEqual(defaultMiddleware)
+    // defaultMiddleware is not modified
+    expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
+  })
+
+  test('allows to prepend multiple values (rest)', () => {
+    const prepended = defaultMiddleware.prepend(middleware1, middleware2)
+
+    // value is prepended
+    expect(prepended).toEqual([middleware1, middleware2, ...defaultMiddleware])
+    // returned value is of correct type
+    expect(prepended).toBeInstanceOf(MiddlewareArray)
+    // prepended is a new array
+    expect(prepended).not.toEqual(defaultMiddleware)
+    // defaultMiddleware is not modified
+    expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
+  })
+
+  test('allows to concat a single value', () => {
+    const concatenated = defaultMiddleware.concat(middleware1)
+
+    // value is concatenated
+    expect(concatenated).toEqual([...defaultMiddleware, middleware1])
+    // returned value is of correct type
+    expect(concatenated).toBeInstanceOf(MiddlewareArray)
+    // concatenated is a new array
+    expect(concatenated).not.toEqual(defaultMiddleware)
+    // defaultMiddleware is not modified
+    expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
+  })
+
+  test('allows to concat multiple values (array as first argument)', () => {
+    const concatenated = defaultMiddleware.concat([middleware1, middleware2])
+
+    // value is concatenated
+    expect(concatenated).toEqual([
+      ...defaultMiddleware,
+      middleware1,
+      middleware2
+    ])
+    // returned value is of correct type
+    expect(concatenated).toBeInstanceOf(MiddlewareArray)
+    // concatenated is a new array
+    expect(concatenated).not.toEqual(defaultMiddleware)
+    // defaultMiddleware is not modified
+    expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
+  })
+
+  test('allows to concat multiple values (rest)', () => {
+    const concatenated = defaultMiddleware.concat(middleware1, middleware2)
+
+    // value is concatenated
+    expect(concatenated).toEqual([
+      ...defaultMiddleware,
+      middleware1,
+      middleware2
+    ])
+    // returned value is of correct type
+    expect(concatenated).toBeInstanceOf(MiddlewareArray)
+    // concatenated is a new array
+    expect(concatenated).not.toEqual(defaultMiddleware)
+    // defaultMiddleware is not modified
+    expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
+  })
+
+  test('allows to concat and then prepend', () => {
+    const concatenated = defaultMiddleware
+      .concat(middleware1)
+      .prepend(middleware2)
+
+    expect(concatenated).toEqual([
+      middleware2,
+      ...defaultMiddleware,
+      middleware1
+    ])
+  })
+
+  test('allows to prepend and then concat', () => {
+    const concatenated = defaultMiddleware
+      .prepend(middleware2)
+      .concat(middleware1)
+
+    expect(concatenated).toEqual([
+      middleware2,
+      ...defaultMiddleware,
+      middleware1
+    ])
+  })
+})
diff --git a/type-tests/files/MiddlewareArray.typetest.ts b/type-tests/files/MiddlewareArray.typetest.ts
new file mode 100644
index 00000000..eb77189e
--- /dev/null
+++ b/type-tests/files/MiddlewareArray.typetest.ts
@@ -0,0 +1,114 @@
+import { getDefaultMiddleware } from 'src/getDefaultMiddleware'
+import { Middleware } from 'redux'
+import { DispatchForMiddlewares } from 'src/tsHelpers'
+
+declare const expectType: <T>(t: T) => T
+
+declare const middleware1: Middleware<{
+  (_: string): number
+}>
+
+declare const middleware2: Middleware<{
+  (_: number): string
+}>
+
+declare const getDispatch: <M extends Array<Middleware>>(
+  m: M
+) => DispatchForMiddlewares<M>
+
+type ThunkReturn = Promise<'thunk'>
+declare const thunkCreator: () => () => ThunkReturn
+
+{
+  const defaultMiddleware = getDefaultMiddleware()
+
+  // prepend single element
+  {
+    const concatenated = defaultMiddleware.prepend(middleware1)
+    const dispatch = getDispatch(concatenated)
+    expectType<number>(dispatch('foo'))
+    expectType<ThunkReturn>(dispatch(thunkCreator()))
+
+    // typings:expect-error
+    expectType<string>(dispatch('foo'))
+  }
+
+  // prepepend multiple (rest)
+  {
+    const concatenated = defaultMiddleware.prepend(middleware1, middleware2)
+    const dispatch = getDispatch(concatenated)
+    expectType<number>(dispatch('foo'))
+    expectType<string>(dispatch(5))
+    expectType<ThunkReturn>(dispatch(thunkCreator()))
+
+    // typings:expect-error
+    expectType<string>(dispatch('foo'))
+  }
+
+  // prepend multiple (array notation)
+  {
+    const concatenated = defaultMiddleware.prepend([
+      middleware1,
+      middleware2
+    ] as const)
+    const dispatch = getDispatch(concatenated)
+    expectType<number>(dispatch('foo'))
+    expectType<string>(dispatch(5))
+    expectType<ThunkReturn>(dispatch(thunkCreator()))
+
+    // typings:expect-error
+    expectType<string>(dispatch('foo'))
+  }
+
+  // concat single element
+  {
+    const concatenated = defaultMiddleware.concat(middleware1)
+    const dispatch = getDispatch(concatenated)
+    expectType<number>(dispatch('foo'))
+    expectType<ThunkReturn>(dispatch(thunkCreator()))
+
+    // typings:expect-error
+    expectType<string>(dispatch('foo'))
+  }
+
+  // prepepend multiple (rest)
+  {
+    const concatenated = defaultMiddleware.concat(middleware1, middleware2)
+    const dispatch = getDispatch(concatenated)
+    expectType<number>(dispatch('foo'))
+    expectType<string>(dispatch(5))
+    expectType<ThunkReturn>(dispatch(thunkCreator()))
+
+    // typings:expect-error
+    expectType<string>(dispatch('foo'))
+  }
+
+  // concat multiple (array notation)
+  {
+    const concatenated = defaultMiddleware.concat([
+      middleware1,
+      middleware2
+    ] as const)
+    const dispatch = getDispatch(concatenated)
+    expectType<number>(dispatch('foo'))
+    expectType<string>(dispatch(5))
+    expectType<ThunkReturn>(dispatch(thunkCreator()))
+
+    // typings:expect-error
+    expectType<string>(dispatch('foo'))
+  }
+
+  // concat and prepend
+  {
+    const concatenated = defaultMiddleware
+      .concat(middleware1)
+      .prepend(middleware2)
+    const dispatch = getDispatch(concatenated)
+    expectType<number>(dispatch('foo'))
+    expectType<string>(dispatch(5))
+    expectType<ThunkReturn>(dispatch(thunkCreator()))
+
+    // typings:expect-error
+    expectType<string>(dispatch('foo'))
+  }
+}
diff --git a/type-tests/files/configureStore.typetest.ts b/type-tests/files/configureStore.typetest.ts
index 957b2432..5b43eed7 100644
--- a/type-tests/files/configureStore.typetest.ts
+++ b/type-tests/files/configureStore.typetest.ts
@@ -1,3 +1,4 @@
+/* eslint-disable no-lone-blocks */
 import {
   applyMiddleware,
   Dispatch,
@@ -9,6 +10,11 @@ import {
 import { configureStore, PayloadAction, getDefaultMiddleware } from 'src'
 import thunk, { ThunkMiddleware, ThunkAction, ThunkDispatch } from 'redux-thunk'
 
+const _anyMiddleware: any = () => () => () => {}
+function expectType<T>(t: T) {
+  return t
+}
+
 /*
  * Test: configureStore() requires a valid reducer or reducer map.
  */
@@ -270,4 +276,118 @@ import thunk, { ThunkMiddleware, ThunkAction, ThunkDispatch } from 'redux-thunk'
     // typings:expect-error
     store.dispatch(function() {} as ThunkAction<void, {}, boolean, AnyAction>)
   }
+
+  /**
+   * 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())
+  }
+
+  /**
+   * Test: custom middleware and getDefaultMiddleware, using prepend
+   */
+  {
+    const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> = _anyMiddleware
+    const concatenated = getDefaultMiddleware<StateA>().prepend(otherMiddleware)
+
+    expectType<
+      ReadonlyArray<typeof otherMiddleware | ThunkMiddleware | Middleware<{}>>
+    >(concatenated)
+
+    const store = configureStore({
+      reducer: reducerA,
+      middleware: concatenated
+    })
+    const result1: 'A' = store.dispatch('a')
+    const result2: Promise<'A'> = store.dispatch(thunkA())
+    // typings:expect-error
+    store.dispatch(thunkB())
+  }
+
+  /**
+   * Test: custom middleware and getDefaultMiddleware, using concat
+   */
+  {
+    const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> = _anyMiddleware
+    const concatenated = getDefaultMiddleware<StateA>().concat(otherMiddleware)
+
+    expectType<
+      ReadonlyArray<typeof otherMiddleware | ThunkMiddleware | Middleware<{}>>
+    >(concatenated)
+
+    const store = configureStore({
+      reducer: reducerA,
+      middleware: concatenated
+    })
+    const result1: 'A' = store.dispatch('a')
+    const result2: Promise<'A'> = store.dispatch(thunkA())
+    // typings:expect-error
+    store.dispatch(thunkB())
+  }
+
+  /**
+   * Test: middlewareBuilder notation, getDefaultMiddleware (unconfigured)
+   */
+  {
+    const store = configureStore({
+      reducer: reducerA,
+      middleware: getDefaultMiddleware =>
+        [
+          ((() => {}) as any) as Middleware<(a: 'a') => 'A', StateA>,
+          ...getDefaultMiddleware()
+        ] as const
+    })
+    const result1: 'A' = store.dispatch('a')
+    const result2: Promise<'A'> = store.dispatch(thunkA())
+    // typings:expect-error
+    store.dispatch(thunkB())
+  }
+
+  /**
+   * Test: middlewareBuilder notation, getDefaultMiddleware, concat & prepend
+   */
+  {
+    const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> = _anyMiddleware
+    const otherMiddleware2: Middleware<(a: 'b') => 'B', StateA> = _anyMiddleware
+    const store = configureStore({
+      reducer: reducerA,
+      middleware: getDefaultMiddleware =>
+        getDefaultMiddleware()
+          .concat(otherMiddleware)
+          .prepend(otherMiddleware2)
+    })
+    const result1: 'A' = store.dispatch('a')
+    const result2: Promise<'A'> = store.dispatch(thunkA())
+    const result3: 'B' = store.dispatch('b')
+    // typings:expect-error
+    store.dispatch(thunkB())
+  }
+
+  /**
+   * Test: middlewareBuilder notation, getDefaultMiddleware (thunk: false)
+   */
+  {
+    const store = configureStore({
+      reducer: reducerA,
+      middleware: getDefaultMiddleware =>
+        [
+          ((() => {}) as any) as Middleware<(a: 'a') => 'A', StateA>,
+          ...getDefaultMiddleware({ thunk: false })
+        ] as const
+    })
+    const result1: 'A' = store.dispatch('a')
+    // typings:expect-error
+    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
deleted file mode 100644
index 4a739947..00000000
--- a/type-tests/files/min-3.4/configureStore.typetest.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-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
index d8c37341..0128e2bd 100644
--- a/type-tests/lib.ts
+++ b/type-tests/lib.ts
@@ -15,3 +15,13 @@ export function checkDirectory(path: string, bail: boolean = false, depth = 1) {
 
   check(files, tsConfigPath, bail)
 }
+
+export function checkFile(path: string, bail: boolean = false, depth = 1) {
+  const tsConfigPath = findConfigFile(path, sys.fileExists)
+
+  if (!tsConfigPath) {
+    throw new Error(`Cannot find TypeScript config file in ${path}.`)
+  }
+
+  check([path], tsConfigPath, bail)
+}
diff --git a/type-tests/types-min-3.4.test.ts b/type-tests/types-min-3.4.test.ts
deleted file mode 100644
index b4c04c6b..00000000
--- a/type-tests/types-min-3.4.test.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { testIf, checkDirectory, tsVersion } from './lib'
-
-testIf(tsVersion >= 3.4)('Types >= 3.4', () => {
-  checkDirectory(`${__dirname}/files/min-3.4`)
-})