Repository: reduxjs/redux-toolkit
Pull request #559 (2020-05-15 13:12:03)

Adding correctly typed `` prepend`  and `` concat`  to the array…
This should solve #552 - opening as a draft PR because I want to re-review this tomorrow myself.

I streamed coding this live and it's available at https://www.youtube.com/watch?v=uQVJ6Aw3tho :smile: 

Also, this is based of #549, so once that is merged, this is probably more readable.

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

Built with commit e056de803b4d95862a80bf48905c19dec445d131

https://deploy-preview-559--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/559/builds/26934) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit e056de803b4d95862a80bf48905c19dec445d131:

|Sandbox| Source |
|--|--|
|[vigilant-feather-1nbub](https://codesandbox.io/s/vigilant-feather-1nbub)| Configuration |
|[ecstatic-hawking-3nz3z](https://codesandbox.io/s/ecstatic-hawking-3nz3z)| Configuration |
|[goofy-monad-t1l94](https://codesandbox.io/s/goofy-monad-t1l94)| Configuration |

phryneas: Okay, moved the `MiddlewareArray` to `utils.ts`, exported it & added documentation. I believe this might be ready for review now.

Patch:
diff --git a/docs/api/createAsyncThunk.md b/docs/api/createAsyncThunk.md
index 42e72243..0a25097d 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`:
 
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..2d2d63c4 100644
--- a/etc/redux-toolkit.api.md
+++ b/etc/redux-toolkit.api.md
@@ -112,7 +112,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>;
 }
@@ -279,7 +279,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 +305,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/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..d3cfbea0 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`
@@ -339,6 +339,7 @@ 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.'
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..e086747b 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 {
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/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`)
-})