Repository: reduxjs/redux-toolkit (ts)

Issue:
Return type of createAsyncThunk
New issue as per #486.

Using essentially the same example from that issue:

```ts
type ThunkApiConfig = {
    dispatch: MyDispatch:
    rejectValue: MyErrorType;
};

export function createAppThunk<Returned, ThunkArg = void>(
    type: string,
    payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg>
): AsyncThunk<Returned, ThunkArg> {
    return createAsyncThunk<Returned, ThunkArg, ThunkApiConfig>(type, payloadCreator);
}
```

In this toy example, `AsyncThunk` (not a real type at the moment) would be the type of the return value of `createAsyncThunk`.  As far as I can tell, there is no way to reference this type in a declaration.  (`ReturnType<typeof createAsyncThunk>` doesn't work because `createAsyncThunk` is generic.)

In this toy example the compiler can infer the type from the function body and so you can omit the return type declaration entirely, but this inference is not always possible.

For example, consider this function definition using overloads:

```ts
export function createApiThunk<Returned, ThunkArg>(
    type: string,
    api: (arg: ThunkArg) => Promise<Returned>
): AsyncThunk<Returned, ThunkArg>;

export function createApiThunk<Returned, ThunkArg, ApiResult>(
    type: string,
    api: (arg: ThunkArg) => Promise<ApiResult>,
    transform: (arg: ApiResult) => Returned
): AsyncThunk<Returned, ThunkArg>;

export function createApiThunk<Returned, ThunkArg, ApiResult>(
    type: string,
    api: (arg: ThunkArg) => Promise<ApiResult>,
    transform?: (arg: ApiResult) => Returned
): AsyncThunk<ApiResult | Returned, ThunkArg> {
    return createAsyncThunk<Returned, ThunkArg, ThunkApiConfig>(type,
        (arg, { rejectWithValue }) => api(arg)
            .then(value => transform ? transform(value) : value)
            .catch((err: MyErrorType) => rejectWithValue(err))
    );   
}
```

In this case, the function overload definitions can't get an inferred return type from the function body, so omitting them would yield a return type of `any`.  Getting strict typing then requires the type `AsyncThunk` to be available.

Pull request description:
Export return type of createAsyncThunk
Resolves #571.

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/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/tutorials/basic-tutorial.md b/docs/tutorials/basic-tutorial.md
index 0126470e..cb6c734a 100644
--- a/docs/tutorials/basic-tutorial.md
+++ b/docs/tutorials/basic-tutorial.md
@@ -157,7 +157,9 @@ function counter(state = 0, action) {
   }
 }
 
-const store = Redux.createStore(counter)
+const store = configureStore({
+  reducer: counter
+})
 
 document.getElementById('increment').addEventListener('click', () => {
   store.dispatch(increment())
diff --git a/docs/usage/usage-with-typescript.md b/docs/usage/usage-with-typescript.md
index ab1a5d46..b3320486 100644
--- a/docs/usage/usage-with-typescript.md
+++ b/docs/usage/usage-with-typescript.md
@@ -55,20 +55,21 @@ const store = configureStore({
 })
 
 export type AppDispatch = typeof store.dispatch
-export const useAppDispatch = () => useDispatch<AppDispatch>() // Export a hook that can be resused to resolve types
+export const useAppDispatch = () => useDispatch<AppDispatch>() // Export a hook that can be reused to resolve types
 ```
 
 ### Correct typings for the `Dispatch` type
 
 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..005aa3da 100644
--- a/etc/redux-toolkit.api.md
+++ b/etc/redux-toolkit.api.md
@@ -59,6 +59,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 +120,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 +132,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?: {
@@ -279,7 +271,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 +297,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/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 8e6b3e2d..cab1d2c8 100644
--- a/src/createAsyncThunk.ts
+++ b/src/createAsyncThunk.ts
@@ -199,12 +199,12 @@ interface AsyncThunkOptions<
    * A method to control whether the asyncThunk should be executed. Has access to the
    * `arg`, `api.getState()` and `api.extra` arguments.
    *
-   * @returns `true` if the asyncThunk should be executed, `false` if it should be skipped
+   * @returns `false` if it should be skipped
    */
   condition?(
     arg: ThunkArg,
     api: Pick<GetThunkAPI<ThunkApiConfig>, 'getState' | 'extra'>
-  ): boolean
+  ): boolean | undefined
   /**
    * If `condition` returns `false`, the asyncThunk will be skipped.
    * This option allows you to control whether a `rejected` action with `meta.condition == false`
@@ -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/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..aa296e66 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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))
+  }
+}


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/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`)
-})