Repository: reduxjs/redux-toolkit
Pull request #374 (2020-02-19 04:42:08)

v1.3.0 Integration
WIP draft PR to collect all our v1.3.0 changes together, giving us docs preview, bundle size checks, etc.

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

Built with commit 219be241d8a33962adeac8dfb73e165b7ebd0341

https://deploy-preview-374--redux-starter-kit-docs.netlify.com
github-actions[bot]: **Size Change:** +14.9 kB (26%) 🚨

**Total Size:** 56.7 kB

| Filename | Size | Change | |
|:--- |:---:|:---:|:---:|
| `dist/redux-toolkit.cjs.development.js` | 7.77 kB | +3.57 kB (45%) | 🚨 |
| `dist/redux-toolkit.cjs.production.min.js` | 3.79 kB | +1.69 kB (44%) | 🚨 |
| `dist/redux-toolkit.esm.js` | 7.58 kB | +3.55 kB (46%) | 🚨 |
| `dist/redux-toolkit.umd.js` | 27.1 kB | +4.33 kB (16%) | ⚠️ |
| `dist/redux-toolkit.umd.min.js` | 10.4 kB | +1.71 kB (16%) | ⚠️ |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size | Change | |
|:--- |:---:|:---:|:---:|
| `dist/index.js` | 149 B | 0 B |  |


</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
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/374/builds/11404) or the icon next to each commit SHA.
markerikson: @phryneas I just saw someone in Discord report that they were getting non-serializable values in an action dispatched by `createAsyncThunk`, in a field like `meta.args.someTimestamp`.  Looks like they're passing a `Date` instance to the payload creator.

This is an interesting issue, because strictly speaking passing non-serializable values to the payload creator _should_ be entirely fine.  It's only a problem because we're attaching those to the dispatched action.

Any thoughts on what the right approach is for dealing with that?  Some possibilities:

- Just document it and say "either don't pass non-serializable args to the payload creator, or explicitly ignore that action type in the serializability check"
- Edit the serializability middleware to ignore `action.meta.args` by default
phryneas: > @phryneas I just saw someone in Discord report that they were getting non-serializable values in an action dispatched by `createAsyncThunk`, in a field like `meta.args.someTimestamp`. Looks like they're passing a `Date` instance to the payload creator.
> 
> This is an interesting issue, because strictly speaking passing non-serializable values to the payload creator _should_ be entirely fine. It's only a problem because we're attaching those to the dispatched action.
> 
> Any thoughts on what the right approach is for dealing with that? Some possibilities:
> 
>     * Just document it and say "either don't pass non-serializable args to the payload creator, or explicitly ignore that action type in the serializability check"
> 
>     * Edit the serializability middleware to ignore `action.meta.args` by default

One step further: have the serializability middleware ignore all `action.meta` by default? Would be a nice escape hatch to pass non-serializable data to middlewares etc without having to stop action propagation.
markerikson: Mmm... the serializable middleware should generally be after all others anyway unless you've reordered things, which is why it's okay to dispatch thunk functions in the first place.

But yeah, let's at least specifically exempt `meta.args`, since that's not even something the user is explicitly adding to the action themselves.
markerikson: 3, 2, 1, let's jam

Patch:
diff --git a/.gitignore b/.gitignore
index 3c2c7e6f..a24899ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ node_modules
 dist
 lib
 es
+yarn.lock
 
 
 .idea/
diff --git a/.travis.yml b/.travis.yml
index 651a9628..6fb7b5e2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,11 +2,10 @@ language: node_js
 node_js: node
 env:
   - TYPESCRIPT_VERSION=rc
+  - TYPESCRIPT_VERSION=3.8
   - TYPESCRIPT_VERSION=3.7
   - TYPESCRIPT_VERSION=3.6
   - TYPESCRIPT_VERSION=3.5
-  - TYPESCRIPT_VERSION=3.4
-  - TYPESCRIPT_VERSION=3.3
 install:
   - npm ci --ignore-scripts
   - npm install typescript@$TYPESCRIPT_VERSION
diff --git a/README.md b/README.md
index ea530ef5..cd09333d 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,6 @@
 [![build status](https://img.shields.io/travis/reduxjs/redux-toolkit/master.svg?style=flat-square)](https://travis-ci.org/reduxjs/redux-toolkit)
 [![npm version](https://img.shields.io/npm/v/@reduxjs/toolkit.svg?style=flat-square)](https://www.npmjs.com/package/@reduxjs/toolkit)
 [![npm downloads](https://img.shields.io/npm/dm/@reduxjs/toolkit.svg?style=flat-square&label=RTK+downloads)](https://www.npmjs.com/package/@reduxjs/toolkit)
-[![npm downloads](https://img.shields.io/npm/dm/redux-starter-kit.svg?style=flat-square&label=RSK+downloads)](https://www.npmjs.com/package/redux-starter-kit)
 
 **The official, opinionated, batteries-included toolset for efficient Redux development**
 
@@ -44,16 +43,18 @@ The **Redux Toolkit** package is intended to be the standard way to write Redux
 
 We can't solve every use case, but in the spirit of [`create-react-app`](https://github.com/facebook/create-react-app) and [`apollo-boost`](https://dev-blog.apollodata.com/zero-config-graphql-state-management-27b1f1b3c2c3), we can try to provide some tools that abstract over the setup process and handle the most common use cases, as well as include some useful utilities that will let the user simplify their application code.
 
-This package is _not_ intended to solve every possible use case for Redux, and is deliberately limited in scope. It does _not_ address concepts like "reusable encapsulated Redux modules", data fetching, folder or file structures, managing entity relationships in the store, and so on.
+Because of that, this package is deliberately limited in scope. It does _not_ address concepts like "reusable encapsulated Redux modules", data caching, folder or file structures, managing entity relationships in the store, and so on.
 
 ## What's Included
 
-Redux Toolkit includes:
+Redux Toolkit includes these APIs:
 
-- A `configureStore()` function with simplified configuration options. It can automatically combine your slice reducers, adds whatever Redux middleware you supply, includes `redux-thunk` by default, and enables use of the Redux DevTools Extension.
-- A `createReducer()` utility that lets you supply a lookup table of action types to case reducer functions, rather than writing switch statements. In addition, it automatically uses the [`immer` library](https://github.com/mweststrate/immer) to let you write simpler immutable updates with normal mutative code, like `state.todos[3].completed = true`.
-- A `createAction()` utility that returns an action creator function for the given action type string. The function itself has `toString()` defined, so that it can be used in place of the type constant.
-- A `createSlice()` function that accepts a set of reducer functions, a slice name, and an initial state value, and automatically generates a slice reducer with corresponding action creators and action types.
+- `configureStore()`: wraps `createStore` to provide simplified configuration options and good defaults. It can automatically combine your slice reducers, adds whatever Redux middleware you supply, includes `redux-thunk` by default, and enables use of the Redux DevTools Extension.
+- `createReducer()`: that lets you supply a lookup table of action types to case reducer functions, rather than writing switch statements. In addition, it automatically uses the [`immer` library](https://github.com/mweststrate/immer) to let you write simpler immutable updates with normal mutative code, like `state.todos[3].completed = true`.
+- `createAction()`: generates an action creator function for the given action type string. The function itself has `toString()` defined, so that it can be used in place of the type constant.
+- `createSlice()`: accepts an object of reducer functions, a slice name, and an initial state value, and automatically generates a slice reducer with corresponding action creators and action types.
+- `createAsyncThunk`: accepts an action type string and a function that returns a promise, and generates a thunk that dispatches `pending/resolved/rejected` action types based on that promise
+- `createEntityAdapter`: generates a set of reusable reducers and selectors to manage normalized data in the store
 - The `createSelector` utility from the [Reselect](https://github.com/reduxjs/reselect) library, re-exported for ease of use.
 
 ## Documentation
diff --git a/docs/api/createAsyncThunk.md b/docs/api/createAsyncThunk.md
new file mode 100644
index 00000000..9e6c7a2c
--- /dev/null
+++ b/docs/api/createAsyncThunk.md
@@ -0,0 +1,567 @@
+---
+id: createAsyncThunk
+title: createAsyncThunk
+sidebar_label: createAsyncThunk
+hide_title: true
+---
+
+# `createAsyncThunk`
+
+## Overview
+
+A function that accepts a Redux action type string and a callback function that should return a promise. It generates promise lifecycle action types based on the provided action type, and returns a thunk action creator that will run the promise callback and dispatch the lifecycle actions based on the returned promise.
+
+This abstracts the standard recommended approach for handling async request lifecycles.
+
+Sample usage:
+
+```js {5-11,22-25,30}
+import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
+import { userAPI } from './userAPI'
+
+// First, create the thunk
+const fetchUserById = createAsyncThunk(
+  'users/fetchByIdStatus',
+  async (userId, thunkAPI) => {
+    const response = await userAPI.fetchById(userId)
+    return response.data
+  }
+)
+
+// Then, handle actions in your reducers:
+const usersSlice = createSlice({
+  name: 'users',
+  initialState: { entities: [], loading: 'idle' },
+  reducers: {
+    // standard reducer logic, with auto-generated action types per reducer
+  },
+  extraReducers: {
+    // Add reducers for additional action types here, and handle loading state as needed
+    [fetchUserById.fulfilled]: (state, action) => {
+      // Add user to the state array
+      state.entities.push(action.payload)
+    }
+  }
+})
+
+// Later, dispatch the thunk as needed in the app
+dispatch(fetchUserById(123))
+```
+
+## Parameters
+
+`createAsyncThunk` accepts two parameters: a string action `type` value, and a `payloadCreator` callback.
+
+### `type`
+
+A string that will be used to generate additional Redux action type constants, representing the lifecycle of an async request:
+
+For example, a `type` argument of `'users/requestStatus'` will generate these action types:
+
+- `pending`: `'users/requestStatus/pending'`
+- `fulfilled`: `'users/requestStatus/fulfilled'`
+- `rejected`: `'users/requestStatus/rejected'`
+
+### `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.
+
+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.
+
+The `payloadCreator` function will be called with two arguments:
+
+- `arg`: a single value, containing the first parameter that was passed to the thunk action creator when it was dispatched. This is useful for passing in values like item IDs that may be needed as part of the request. If you need to pass in multiple values, pass them together in an object when you dispatch the thunk, like `dispatch(fetchUsers({status: 'active', sortBy: 'name'}))`.
+- `thunkAPI`: an object containing all of the parameters that are normally passed to a Redux thunk function, as well as additional options:
+  - `dispatch`: the Redux store `dispatch` method
+  - `getState`: the Redux store `getState` method
+  - `extra`: the "extra argument" given to the thunk middleware on setup, if available
+  - `requestId`: a unique string ID value that was automatically generated to identify this request sequence
+  - `signal`: an [`AbortController.signal` object](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal) that may be used to see if another part of the app logic has marked this request as needing cancelation.
+  - `rejectWithValue`: rejectWithValue is a utility function that you can `return` in your action creator to return a rejected response with a defined payload. It will pass whatever value you give it and return it in the payload of the rejected action.
+
+The logic in the `payloadCreator` function may use any of these values as needed to calculate the result.
+
+## Return Value
+
+`createAsyncThunk` returns a standard Redux thunk action creator. The thunk action creator function will have plain action creators for the `pending`, `fulfilled`, and `rejected` cases attached as nested fields.
+
+When dispatched, the thunk will:
+
+- dispatch the `pending` action
+- call the `payloadCreator` callback and wait for the returned promise to settle
+- when the promise settles:
+  - if the promise resolved successfully, dispatch the `fulfilled` action with the promise value as `action.payload`
+  - if the promise resolved with a `rejectWithValue(value)` return value, dispatch the `rejected` action with the value passed into `action.payload` and 'Rejected' as `action.error.message`
+  - if the promise failed and was not handled with `rejectWithValue`, dispatch the `rejected` action with a serialized version of the error value as `action.error`
+- Return a fulfilled promise containing the final dispatched action (either the `fulfilled` or `rejected` action object)
+
+## Promise Lifecycle Actions
+
+`createAsyncThunk` will generate three Redux action creators using [`createAction`](./createAction.md): `pending`, `fulfilled`, and `rejected`. Each lifecycle action creator will be attached to the returned thunk action creator so that your reducer logic can reference the action types and respond to the actions when dispatched. Each action object will contain the current unique `requestId` and `args` values under `action.meta`.
+
+The action creators will have these signatures:
+
+```typescript
+interface SerializedError {
+  name?: string
+  message?: string
+  code?: string
+  stack?: string
+}
+
+interface PendingAction<ThunkArg> {
+  type: string
+  payload: undefined
+  meta: {
+    requestId: string
+    arg: ThunkArg
+  }
+}
+
+interface FulfilledAction<ThunkArg, PromiseResult> {
+  type: string
+  payload: PromiseResult
+  meta: {
+    requestId: string
+    arg: ThunkArg
+  }
+}
+
+interface RejectedAction<ThunkArg> {
+  type: string
+  payload: undefined
+  error: SerializedError | any
+  meta: {
+    requestId: string
+    arg: ThunkArg
+    aborted: boolean
+  }
+}
+
+interface RejectedWithValueAction<ThunkArg, RejectedValue> {
+  type: string
+  payload: RejectedValue
+  error: { message: 'Rejected' }
+  meta: {
+    requestId: string
+    arg: ThunkArg
+    aborted: boolean
+  }
+}
+
+type Pending = <ThunkArg>(
+  requestId: string,
+  arg: ThunkArg
+) => PendingAction<ThunkArg>
+
+type Fulfilled = <ThunkArg, PromiseResult>(
+  payload: PromiseResult,
+  requestId: string,
+  arg: ThunkArg
+) => FulfilledAction<ThunkArg, PromiseResult>
+
+type Rejected = <ThunkArg>(
+  requestId: string,
+  arg: ThunkArg
+) => RejectedAction<ThunkArg>
+
+type RejectedWithValue = <ThunkArg, RejectedValue>(
+  requestId: string,
+  arg: ThunkArg
+) => RejectedWithValueAction<ThunkArg, RejectedValue>
+```
+
+To handle these actions in your reducers, reference the action creators in `createReducer` or `createSlice` using either the object key notation or the "builder callback" notation:
+
+```js {2,6,14,23}
+const reducer1 = createReducer(initialState, {
+  [fetchUserById.fulfilled]: (state, action) => {}
+})
+
+const reducer2 = createReducer(initialState, build => {
+  builder.addCase(fetchUserById.fulfilled, (state, action) => {})
+})
+
+const reducer3 = createSlice({
+  name: 'users',
+  initialState,
+  reducers: {},
+  extraReducers: {
+    [fetchUserById.fulfilled]: (state, action) => {}
+  }
+})
+
+const reducer4 = createSlice({
+  name: 'users',
+  initialState,
+  reducers: {},
+  extraReducers: builder => {
+    builder.addCase(fetchUserById.fulfilled, (state, action) => {})
+  }
+})
+```
+
+## Handling Thunk Results
+
+Thunks may return a value when dispatched. A common use case is to return a promise from the thunk, dispatch the thunk from a component, and then wait for the promise to resolve before doing additional work:
+
+```js
+const onClick = () => {
+  dispatch(fetchUserById(userId)).then(() => {
+    // do additional work
+  })
+}
+```
+
+The thunks generated by `createAsyncThunk` will always return a resolved promise with either the `fulfilled` action object or `rejected` action object inside, as appropriate.
+
+The calling logic may wish to treat these actions as if they were the original promise contents. Redux Toolkit exports an `unwrapResult` function that can be used to extract the `payload` or `error` from the action and return or throw the result:
+
+```js
+import { unwrapResult } from '@reduxjs/toolkit'
+
+// in the component
+const onClick = () => {
+  dispatch(fetchUserById(userId))
+    .then(unwrapResult)
+    .then(originalPromiseResult => {})
+    .catch(serializedError => {})
+}
+```
+
+## Cancellation
+
+If you want to cancel your running thunk before it has finished, you can use the `abort` method of the promise returned by `dispatch(fetchUserById(userId))`.
+
+A real-life example of that would look like this:
+
+```ts
+function MyComponent(props: { userId: string }) {
+  React.useEffect(() => {
+    // Dispatching the thunk returns a promise
+    const promise = dispatch(fetchUserById(props.userId))
+    return () => {
+      // `createAsyncThunk` attaches an `abort()` method to the promise
+      promise.abort()
+    }
+  }, [props.userId])
+}
+```
+
+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.
+
+The `fetch` api of modern browsers already comes with support for an `AbortSignal`:
+
+```ts
+const fetchUserById = createAsyncThunk(
+  'users/fetchById',
+  async (userId, thunkAPI) => {
+    const response = await fetch(`https://reqres.in/api/users/${userId}`, {
+      signal: thunkAPI.signal
+    })
+    return await response.json()
+  }
+)
+```
+
+### Checking Cancellation Status
+
+### Reading the Signal Value
+
+You can use the `signal.aborted` property to regularly check if the thunk has been aborted and in that case stop costly long-running work:
+
+```ts
+const readStream = createAsyncThunk('readStream', async (stream: ReadableStream, {signal}) => {
+  const reader = stream.getReader();
+
+  let done = false;
+  let result = "";
+
+  while (!done) {
+    if (signal.aborted) {
+      throw new Error("stop the work, this has been aborted!");
+    }
+    const read = await reader.read();
+    result += read.value;
+    done = read.done;
+  }
+  return result;
+}
+```
+
+#### Listening for Abort Events
+
+You can also call `signal.addEventListener('abort', callback)` to have logic inside the thunk be notified when `promise.abort()` was called.
+This can for example be used in conjunction with an axios `CancelToken`:
+
+```ts
+import { createAsyncThunk } from '@reduxjs/toolkit'
+import axios from 'axios'
+
+const fetchUserById = createAsyncThunk(
+  'users/fetchById',
+  async (userId, { signal }) => {
+    const source = axios.CancelToken.source()
+    signal.addEventListener('abort', () => {
+      source.cancel()
+    })
+    const response = await axios.get(`https://reqres.in/api/users/${userId}`, {
+      cancelToken: source.token
+    })
+    return response.data
+  }
+)
+```
+
+## Examples
+
+- Requesting a user by ID, with loading state, and only one request at a time:
+
+```js
+import { createAsyncThunk, createSlice, unwrapResult } from '@reduxjs/toolkit'
+import { userAPI } from './userAPI'
+
+const fetchUserById = createAsyncThunk(
+  'users/fetchByIdStatus',
+  async (userId, { getState, requestId }) => {
+    const { currentRequestId, loading } = getState().users
+    if (loading !== 'pending' || requestId !== currentRequestId) {
+      return
+    }
+    const response = await userAPI.fetchById(userId)
+    return response.data
+  }
+)
+
+const usersSlice = createSlice({
+  name: 'users',
+  initialState: {
+    entities: [],
+    loading: 'idle',
+    currentRequestId: undefined,
+    error: null
+  },
+  reducers: {},
+  extraReducers: {
+    [fetchUserById.pending]: (state, action) => {
+      if (state.loading === 'idle') {
+        state.loading = 'pending'
+        state.currentRequestId = action.meta.requestId
+      }
+    },
+    [fetchUserById.fulfilled]: (state, action) => {
+      const { requestId } = action.meta
+      if (state.loading === 'pending' && state.currentRequestId === requestId) {
+        state.loading = 'idle'
+        state.entities.push(action.payload)
+        state.currentRequestId = undefined
+      }
+    },
+    [fetchUserById.rejected]: (state, action) => {
+      const { requestId } = action.meta
+      if (state.loading === 'pending' && state.currentRequestId === requestId) {
+        state.loading = 'idle'
+        state.error = action.error
+        state.currentRequestId = undefined
+      }
+    }
+  }
+})
+
+const UsersComponent = () => {
+  const { users, loading, error } = useSelector(state => state.users)
+  const dispatch = useDispatch()
+
+  const fetchOneUser = async userId => {
+    try {
+      const resultAction = await dispatch(fetchUserById(userId))
+      const user = unwrapResult(resultAction)
+      showToast('success', `Fetched ${user.name}`)
+    } catch (err) {
+      showToast('error', `Fetch failed: ${err.message}`)
+    }
+  }
+
+  // render UI here
+}
+```
+
+- Using rejectWithValue to access a custom rejected payload in a component
+
+```js
+import { createAsyncThunk, createSlice, unwrapResult } from '@reduxjs/toolkit'
+import { userAPI } from './userAPI'
+
+const updateUser = createAsyncThunk(
+  'users/update',
+  async (userData, { rejectWithValue }) => {
+    const { id, ...fields } = userData
+    try {
+      const response = await userAPI.updateById(id, fields)
+      return response.data.user
+    } catch (err) {
+      // Note: this is an example assuming the usage of axios. Other fetching libraries would likely have different implementations
+      if (!err.response) {
+        throw err
+      }
+
+      return rejectWithValue(err.response.data)
+    }
+  }
+)
+
+const usersSlice = createSlice({
+  name: 'users',
+  initialState: {
+    entities: {},
+    error: null
+  },
+  reducers: {},
+  extraReducers: {
+    [updateUser.fullfilled]: (state, action) => {
+      const user = action.payload
+      state.entities[user.id] = user
+    },
+    [updateUser.rejected]: (state, action) => {
+      if (action.payload) {
+        // If a rejected action has a payload, it means that it was returned with rejectWithValue
+        state.error = action.payload.errorMessage
+      } else {
+        state.error = action.error
+      }
+    }
+  }
+})
+
+const UsersComponent = () => {
+  const { users, loading, 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
+  const handleUpdateUser = async (values, formikHelpers) => {
+    const resultAction = await dispatch(updateUser(values))
+    if (updateUser.fulfilled.match(resultAction)) {
+      const user = unwrapResult(resultAction)
+      showToast('success', `Updated ${user.name}`)
+    } else {
+      if (resultAction.payload) {
+        // This is assuming the api returned a 400 error with a body of { errorMessage: 'Validation errors', field_errors: { field_name: 'Should be a string' } }
+        formikHelpers.setErrors(resultAction.payload.field_errors)
+      } else {
+        showToast('error', `Update failed: ${resultAction.error}`)
+      }
+    }
+  }
+
+  // render UI here
+}
+```
+
+- TypeScript: Using rejectWithValue to access a custom rejected payload in a component
+  _Note: this is a contrived example assuming our userAPI only ever throws validation-specific errors_
+
+```typescript
+import { createAsyncThunk, createSlice, unwrapResult } from '@reduxjs/toolkit'
+import { userAPI } from './userAPI'
+import { AppDispatch, RootState } from '../store'
+import { FormikHelpers } from 'formik'
+
+// Sample types that will be used
+interface User {
+  first_name: string
+  last_name: string
+  email: string
+}
+
+interface ValidationErrors {
+  errorMessage: string
+  field_errors: Record<string, string>
+}
+
+interface UpdateUserResponse {
+  user: User
+  success: boolean
+}
+
+const updateUser = createAsyncThunk<
+  User,
+  Partial<User>,
+  {
+    rejectValue: ValidationErrors
+  }
+>('users/update', async (userData, { rejectWithValue }) => {
+  try {
+    const { id, ...fields } = userData
+    const response = await userAPI.updateById<UpdateUserResponse>(id, fields)
+    return response.data.user
+  } catch (err) {
+    let error: AxiosError<ValidationErrors> = err // cast the error for access
+    if (!error.response) {
+      throw err
+    }
+    // We got validation errors, let's return those so we can reference in our component and set form errors
+    return rejectWithValue(error.response.data)
+  }
+})
+
+interface UsersState {
+  error: string | null
+  entities: Record<string, User>
+}
+
+const initialState: UsersState = {
+  entities: {},
+  error: null
+}
+
+const usersSlice = createSlice({
+  name: 'users',
+  initialState,
+  reducers: {},
+  extraReducers: builder => {
+    // The `builder` callback form is used here because it provides correctly typed reducers from the action creators
+    builder.addCase(updateUser.fulfilled, (state, { payload }) => {
+      state.entities[payload.id] = payload
+    })
+    builder.addCase(updateUser.rejected, (state, action) => {
+      if (action.payload) {
+        // Being that we passed in ValidationErrors to rejectType in `createAsyncThunk`, the payload will be available here.
+        state.error = action.payload.errorMessage
+      } else {
+        state.error = action.error
+      }
+    })
+  }
+})
+
+const UsersComponent = () => {
+  const { users, loading, 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
+  const handleUpdateUser = async (
+    values: FormValues,
+    formikHelpers: FormikHelpers<FormValues>
+  ) => {
+    const resultAction = await dispatch(updateUser(values))
+    if (updateUser.fulfilled.match(resultAction)) {
+      // user will have a type signature of User as we passed that as the Returned parameter in createAsyncThunk
+      const user = unwrapResult(resultAction)
+      showToast('success', `Updated ${user.name}`)
+    } else {
+      if (resultAction.payload) {
+        // Being that we passed in ValidationErrors to rejectType in `createAsyncThunk`, those types will be available here.
+        formikHelpers.setErrors(resultAction.payload.field_errors)
+      } else {
+        showToast('error', `Update failed: ${resultAction.error}`)
+      }
+    }
+  }
+
+  // render UI here
+}
+```
diff --git a/docs/api/createEntityAdapter.md b/docs/api/createEntityAdapter.md
new file mode 100644
index 00000000..e13e7de8
--- /dev/null
+++ b/docs/api/createEntityAdapter.md
@@ -0,0 +1,378 @@
+---
+id: createEntityAdapter
+title: createEntityAdapter
+sidebar_label: createEntityAdapter
+hide_title: true
+---
+
+# `createEntityAdapter`
+
+## Overview
+
+A function that generates a set of prebuilt reducers and selectors for performing CRUD operations on a [normalized state structure](https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape) containing instances of a particular type of data object. These reducer functions may be passed as case reducers to `createReducer` and `createSlice`. They may also be used as "mutating" helper functions inside of `createReducer` and `createSlice`.
+
+This API was ported from [the `@ngrx/entity` library](https://ngrx.io/guide/entity) created by the NgRx maintainers, but has been significantly modified for use with Redux Toolkit. We'd like to thank the NgRx team for originally creating this API and allowing us to port and adapt it for our needs.
+
+> **Note**: The term "Entity" is used to refer to a unique type of data object in an application. For example, in a blogging application, you might have `User`, `Post`, and `Comment` data objects, with many instances of each being stored in the client and persisted on the server. `User` is an "entity" - a unique type of data object that the application uses. Each unique instance of an entity is assumed to have a unique ID value in a specific field.
+>
+> As with all Redux logic, [_only_ plain JS objects and arrays should be passed in to the store - **no class instances!**](https://redux.js.org/style-guide/style-guide#do-not-put-non-serializable-values-in-state-or-actions)
+>
+> For purposes of this reference, we will use `Entity` to refer to the specific data type that is being managed by a copy of the reducer logic in a specific portion of the Redux state tree, and `entity` to refer to a single instance of that type. Example: in `state.users`, `Entity` would refer to the `User` type, and `state.users.entities[123]` would be a single `entity`.
+
+The methods generated by `createEntityAdapter` will all manipulate an "entity state" structure that looks like:
+
+```js
+{
+  // The unique IDs of each item. Must be strings or numbers
+  ids: []
+  // A lookup table mapping entity IDs to the corresponding entity objects
+  entities: {
+  }
+}
+```
+
+`createEntityAdapter` may be called multiple times in an application. If you are using it with plain JavaScript, you may be able to reuse a single adapter definition with multiple entity types if they're similar enough (such as all having an `entity.id` field). For TypeScript usage, you will need to call `createEntityAdapter` a separate time for each distinct `Entity` type, so that the type definitions are inferred correctly.
+
+Sample usage:
+
+```js
+import {
+  createEntityAdapter,
+  createSlice,
+  configureStore
+} from '@reduxjs/toolkit'
+
+const booksAdapter = createEntityAdapter({
+  // Assume IDs are stored in a field other than `book.id`
+  selectId: book => book.bookId,
+  // Keep the "all IDs" array sorted based on book titles
+  sortComparer: (a, b) => a.title.localeCompare(b.title)
+})
+
+const booksSlice = createSlice({
+  name: 'books',
+  initialState: booksAdapter.getInitialState(),
+  reducers: {
+    // Can pass adapter functions directly as case reducers.  Because we're passing this
+    // as a value, `createSlice` will auto-generate the `bookAdded` action type / creator
+    bookAdded: booksAdapter.addOne,
+    booksReceived(state, action) {
+      // Or, call them as "mutating" helpers in a case reducer
+      booksAdapter.setAll(state, action.payload.books)
+    }
+  }
+})
+
+const store = configureStore({
+  reducer: {
+    books: booksSlice.reducer
+  }
+})
+
+console.log(store.getState().books)
+// {ids: [], entities: {} }
+
+// Can create a set of memoized selectors based on the location of this entity state
+const booksSelectors = booksAdapter.getSelectors(state => state.books)
+
+// And then use the selectors to retrieve values
+const allBooks = booksSelectors.selectAll(store.getState())
+```
+
+## Parameters
+
+`createEntityAdapter` accepts a single options object parameter, with two optional fields inside.
+
+### `selectId`
+
+A function that accepts a single `Entity` instance, and returns the value of whatever unique ID field is inside. If not provided, the default implementation is `entity => entity.id`. If your `Entity` type keeps its unique ID values in a field other than `entity.id`, you **must** provide a `selectId` function.
+
+### `sortComparer`
+
+A callback function that accepts two `Entity` instances, and should return a standard `Array.sort()` numeric result (1, 0, -1) to indicate their relative order for sorting.
+
+If provided, the `state.ids` array will be kept in sorted order based on comparisons of the entity objects, so that mapping over the IDs array to retrieve entities by ID should result in a sorted array of entities.
+
+If not provided, the `state.ids` array will not be sorted, and no guarantees are made about the ordering.
+
+## Return Value
+
+A "entity adapter" instance. An entity adapter is a plain JS object (not a class) containing the generated reducer functions, the original provided `selectId` and `sortComparer` callbacks, a method to generate an initial "entity state" value, and functions to generate a set of globalized and non-globalized memoized selector functions for this entity type.
+
+The adapter instance will include the following methods (additional referenced TypeScript types included):
+
+```ts
+export type EntityId = number | string
+
+export type Comparer<T> = (a: T, b: T) => number
+
+export type IdSelector<T> = (model: T) => EntityId
+
+export interface DictionaryNum<T> {
+  [id: number]: T | undefined
+}
+
+export interface Dictionary<T> extends DictionaryNum<T> {
+  [id: string]: T | undefined
+}
+
+export type Update<T> = { id: EntityId; changes: Partial<T> }
+
+export interface EntityState<T> {
+  ids: EntityId[]
+  entities: Dictionary<T>
+}
+
+export interface EntityDefinition<T> {
+  selectId: IdSelector<T>
+  sortComparer: false | Comparer<T>
+}
+
+export interface EntityStateAdapter<T> {
+  addOne<S extends EntityState<T>>(state: S, entity: T): S
+  addOne<S extends EntityState<T>>(state: S, action: PayloadAction<T>): S
+
+  addMany<S extends EntityState<T>>(state: S, entities: T[]): S
+  addMany<S extends EntityState<T>>(state: S, entities: PayloadAction<T[]>): S
+
+  setAll<S extends EntityState<T>>(state: S, entities: T[]): S
+  setAll<S extends EntityState<T>>(state: S, entities: PayloadAction<T[]>): S
+
+  removeOne<S extends EntityState<T>>(state: S, key: EntityId): S
+  removeOne<S extends EntityState<T>>(state: S, key: PayloadAction<EntityId>): S
+
+  removeMany<S extends EntityState<T>>(state: S, keys: EntityId[]): S
+  removeMany<S extends EntityState<T>>(
+    state: S,
+    keys: PayloadAction<EntityId[]>
+  ): S
+
+  removeAll<S extends EntityState<T>>(state: S): S
+
+  updateOne<S extends EntityState<T>>(state: S, update: Update<T>): S
+  updateOne<S extends EntityState<T>>(
+    state: S,
+    update: PayloadAction<Update<T>>
+  ): S
+
+  updateMany<S extends EntityState<T>>(state: S, updates: Update<T>[]): S
+  updateMany<S extends EntityState<T>>(
+    state: S,
+    updates: PayloadAction<Update<T>[]>
+  ): S
+
+  upsertOne<S extends EntityState<T>>(state: S, entity: T): S
+  upsertOne<S extends EntityState<T>>(state: S, entity: PayloadAction<T>): S
+
+  upsertMany<S extends EntityState<T>>(state: S, entities: T[]): S
+  upsertMany<S extends EntityState<T>>(
+    state: S,
+    entities: PayloadAction<T[]>
+  ): S
+}
+
+export interface EntitySelectors<T, V> {
+  selectIds: (state: V) => EntityId[]
+  selectEntities: (state: V) => Dictionary<T>
+  selectAll: (state: V) => T[]
+  selectTotal: (state: V) => number
+  selectById: (state: V, id: EntityId) => T | undefined
+}
+
+export interface EntityAdapter<T> extends EntityStateAdapter<T> {
+  selectId: IdSelector<T>
+  sortComparer: false | Comparer<T>
+  getInitialState(): EntityState<T>
+  getInitialState<S extends object>(state: S): EntityState<T> & S
+  getSelectors(): EntitySelectors<T, EntityState<T>>
+  getSelectors<V>(
+    selectState: (state: V) => EntityState<T>
+  ): EntitySelectors<T, V>
+}
+```
+
+### CRUD Functions
+
+The primary content of an entity adapter is a set of generated reducer functions for adding, updating, and removing entity instances from an entity state object:
+
+- `addOne`: accepts a single entity, and adds it
+- `addMany`: accepts an array of entities, and adds them
+- `setAll`: accepts an array of entities, and replaces the existing entity contents with the values in the array
+- `removeOne`: accepts a single entity ID value, and removes the entity with that ID if it exists
+- `removeMany`: accepts an array of entity ID values, and removes each entity with those IDs if they exist
+- `updateOne`: accepts an "update object" containing an entity ID and an object containing one or more new field values to update inside a `changes` field, and updates the corresponding entity
+- `updateMany`: accepts an array of update objects, and updates all corresponding entities
+- `upsertOne`: accepts a single entity. If an entity with that ID exists, the fields in the update will be merged into the existing entity, with any matching fields overwriting the existing values. If the entity does not exist, it will be added.
+- `upsertMany`: accepts an array of entities that will be upserted.
+
+Each method has a signature that looks like:
+
+```ts
+(state: EntityState<T>, argument: TypeOrPayloadAction<Argument<T>>) => EntityState<T>
+```
+
+In other words, they accept a state that looks like `{ids: [], entities: {}}`, and calculate and return a new state.
+
+These CRUD methods may be used in multiple ways:
+
+- They may be passed as case reducers directly to `createReducer` and `createSlice`.
+- They may be used as "mutating" helper methods when called manually, such as a separate hand-written call to `addOne()` inside of an existing case reducer, if the `state` argument is actually an Immer `Draft` value
+- They may be used as immutable update methods when called manually, if the `state` argument is actually a plain JS object or array
+
+> **Note**: These methods do _not_ have corresponding Redux actions created - they are just standalone reducers / update logic. **It is entirely up to you to decide where and how to use these methods!** Most of the time, you will want to pass them to `createSlice` or use them inside another reducer.
+
+Each method will check to see if the `state` argument is an Immer `Draft` or not. If it is a draft, the method will assume that it's safe to continue mutating that draft further. If it is not a draft, the method will pass the plain JS value to Immer's `createNextState()`, and return the immutably updated result value.
+
+The `argument` may be either a plain value (such as a single `Entity` object for `addOne()` or an `Entity[]` array for `addMany()`), or a `PayloadAction` action object with that same value as`action.payload`. This enables using them as both helper functions and reducers.
+
+### `getInitialState`
+
+Returns a new entity state object like `{ids: [], entities: {}}`.
+
+It accepts an optional object as an argument. The fields in that object will be merged into the returned initial state value. For example, perhaps you want your slice to also track some loading state:
+
+```js
+const booksSlice = createSlice({
+  name: 'books',
+  initialState: booksAdapter.getInitialState({
+    loading: 'idle'
+  }),
+  reducers: {
+    booksLoadingStarted(state, action) {
+      // Can update the additional state field
+      state.loading = 'pending'
+    }
+  }
+})
+```
+
+### Selector Functions
+
+The entity adapter will contain a `getSelectors()` function that returns a set of selectors that know how to read the contents of an entity state object:
+
+- `selectIds`: returns the `state.ids` array
+- `selectEntities`: returns the `state.entities` lookup table
+- `selectAll`: maps over the `state.ids` array, and returns an array of entities in the same order
+- `selectTotal`: returns the total number of entities being stored in this state
+- `selectById`: given the state and an entity ID, returns the entity with that ID or `undefined`
+
+Each selector function will be created using the `createSelector` function from Reselect, to enable memoizing calculation of the results.
+
+Because selector functions are dependent on knowing where in the state tree this specific entity state object is kept, `getSelectors()` can be called in two ways:
+
+- If called without any arguments, it returns an "unglobalized" set of selector functions that assume their `state` argument is the actual entity state object to read from
+- It may also be called with a selector function that accepts the entire Redux state tree and returns the correct entity state object.
+
+For example, the entity state for a `Book` type might be kept in the Redux state tree as `state.books`. You can use `getSelectors()` to read from that state in two ways:
+
+```js
+const store = configureStore({
+  reducer: {
+    books: booksReducer
+  }
+})
+
+const simpleSelectors = booksAdapter.getSelectors()
+const globalizedSelectors = booksAdapter.getSelectors(state => state.books)
+
+// Need to manually pass the correct entity state object in to this selector
+const bookIds = simpleSelectors.selectIds(store.getState().books)
+
+// This selector already knows how to find the books entity state
+const allBooks = globalizedSelectors.selectAll(store.getState())
+```
+
+## Notes
+
+### Applying Multiple Updates
+
+If `updateMany()` is called with multiple updates targeted to the same ID, they will be merged into a single update, with later updates overwriting the earlier ones.
+
+For both `updateOne()` and `updateMany()`, changing the ID of one existing entity to match the ID of a second existing entity will cause the first to replace the second completely.
+
+## Examples
+
+Exercising several of the CRUD methods and selectors:
+
+```js
+import {
+  createEntityAdapter,
+  createSlice,
+  configureStore
+} from '@reduxjs/toolkit'
+
+// Since we don't provide `selectId`, it defaults to assuming `entity.id` is the right field
+const booksAdapter = createEntityAdapter({
+  // Keep the "all IDs" array sorted based on book titles
+  sortComparer: (a, b) => a.title.localeCompare(b.title)
+})
+
+const booksSlice = createSlice({
+  name: 'books',
+  initialState: booksAdapter.getInitialState({
+    loading: 'idle'
+  }),
+  reducer: {
+    // Can pass adapter functions directly as case reducers.  Because we're passing this
+    // as a value, `createSlice` will auto-generate the `bookAdded` action type / creator
+    bookAdded: booksAdapter.addOne,
+    booksLoading(state, action) {
+      if (state.loading === 'idle') {
+        state.loading = 'pending'
+      }
+    },
+    booksReceived(state, action) {
+      if (state.loading === 'pending') {
+        // Or, call them as "mutating" helpers in a case reducer
+        booksAdapter.setAll(state, action.payload.books)
+        state.loading = 'idle'
+      }
+    },
+    bookUpdated: booksAdapter.updateOne
+  }
+})
+
+const {
+  bookAdded,
+  booksLoading,
+  booksReceived,
+  bookUpdated
+} = booksSlice.actions
+
+const store = configureStore({
+  reducer: {
+    books: booksSlice.reducer
+  }
+})
+
+// Check the initial state:
+console.log(store.getState().books)
+// {ids: [], entities: {}, loading: 'idle' }
+
+const booksSelectors = booksAdapter.getSelectors(state => state.books)
+
+store.dispatch(bookAdded({ id: 'a', title: 'First' }))
+console.log(store.getState().books)
+// {ids: ["a"], entities: {a: {id: "a", title: "First"}}, loading: 'idle' }
+
+store.dispatch(bookUpdated({ id: 'a', title: 'First (altered)' }))
+store.dispatch(booksLoading())
+console.log(store.getState().books)
+// {ids: ["a"], entities: {a: {id: "a", title: "First (altered)"}}, loading: 'pending' }
+
+store.dispatch(
+  booksReceived([
+    { id: 'b', title: 'Book 3' },
+    { id: 'c', title: 'Book 2' }
+  ])
+)
+
+console.log(booksSelectors.selectIds(store.getState()))
+// "a" was removed due to the `setAll()` call
+// Since they're sorted by title, "Book 2" comes before "Book 3"
+// ["c", "b"]
+
+console.log(booksSelectors.selectAll(store.getState()))
+// All book entries in sorted order
+// [{id: "c", title: "Book 2"}, {id: "b", title: "Book 3"}]
+```
diff --git a/docs/api/getDefaultMiddleware.md b/docs/api/getDefaultMiddleware.md
index adf9c424..ac51a106 100644
--- a/docs/api/getDefaultMiddleware.md
+++ b/docs/api/getDefaultMiddleware.md
@@ -55,12 +55,13 @@ One of the goals of Redux Toolkit is to provide opinionated defaults and prevent
 `getDefaultMiddleware` includes some middleware that are added **in development builds of your app only** to
 provide runtime checks for two common issues:
 
-- [`redux-immutable-state-invariant`](https://github.com/leoasis/redux-immutable-state-invariant): deeply compares
+- [`immutable-state-invariant`](./otherExports.md#createimmutablestateinvariantmiddleware): deeply compares
   state values for mutations. It can detect mutations in reducers during a dispatch, and also mutations that occur between
   dispatches (such as in a component or a selector). When a mutation is detected, it will throw an error and indicate the key
-  path for where the mutated value was detected in the state tree.
+  path for where the mutated value was detected in the state tree. (Forked from [`redux-immutable-state-invariant`](https://github.com/leoasis/redux-immutable-state-invariant).)
+
 - [`serializable-state-invariant-middleware`](./otherExports.md#createserializablestateinvariantmiddleware): a custom middleware created specifically for use in Redux Toolkit. Similar in
-  concept to `redux-immutable-state-invariant`, but deeply checks your state tree and your actions for non-serializable values
+  concept to `immutable-state-invariant`, but deeply checks your state tree and your actions for non-serializable values
   such as functions, Promises, Symbols, and other non-plain-JS-data values. When a non-serializable value is detected, a
   console error will be printed with the key path for where the non-serializable value was detected.
 
diff --git a/docs/api/otherExports.md b/docs/api/otherExports.md
index 61761298..615f68d5 100644
--- a/docs/api/otherExports.md
+++ b/docs/api/otherExports.md
@@ -11,6 +11,56 @@ Redux Toolkit exports some of its internal utilities, and re-exports additional
 
 ## Internal Exports
 
+### `createImmutableStateInvariantMiddleware`
+
+Creates an instance of the `immutable-state-invariant` middleware described in [`getDefaultMiddleware`](./getDefaultMiddleware.md).
+
+Accepts a single configuration object parameter, with the following options:
+
+```ts
+function createImmutableStateInvariantMiddleware({
+  // The function to check if a value is considered to be immutable.
+  // This function is applied recursively to every value contained in the state.
+  // The default implementation will return true for primitive types (like numbers, strings, booleans, null and undefined).
+  isImmutable?: (value: any) => boolean
+  // An array of dot-separated path strings that match named nodes from the root state to ignore when checking for immutability.
+  // Defaults to undefined
+  ignoredPaths?: string[]
+})
+```
+
+Example:
+
+```js
+import {
+  createSlice,
+  configureStore,
+  createImmutableStateInvariantMiddleware
+} from '@reduxjs/toolkit'
+
+const exampleSlice = createSlice({
+  name: 'example',
+  initialState: {
+    user: 'will track changes',
+    ignoredPath: 'single level',
+    ignoredNested: {
+      one: 'one',
+      two: 'two'
+    }
+  },
+  reducers: {}
+})
+
+const immutableInvariantMiddleware = createImmutableStateInvariantMiddleware({
+  ignoredPaths: ['ignoredPath', 'ignoredNested.one', 'ignoredNested.two']
+})
+
+const store = configureStore({
+  reducer: exampleSlice.reducer,
+  middleware: [immutableInvariantMiddleware]
+})
+```
+
 ### `createSerializableStateInvariantMiddleware`
 
 Creates an instance of the `serializable-state-invariant` middleware described in [`getDefaultMiddleware`](./getDefaultMiddleware.md).
diff --git a/docs/introduction/quick-start.md b/docs/introduction/quick-start.md
index 39a64298..b4c042e4 100644
--- a/docs/introduction/quick-start.md
+++ b/docs/introduction/quick-start.md
@@ -17,7 +17,7 @@ The **Redux Toolkit** package is intended to be the standard way to write Redux
 
 We can't solve every use case, but in the spirit of [`create-react-app`](https://github.com/facebook/create-react-app) and [`apollo-boost`](https://dev-blog.apollodata.com/zero-config-graphql-state-management-27b1f1b3c2c3), we can try to provide some tools that abstract over the setup process and handle the most common use cases, as well as include some useful utilities that will let the user simplify their application code.
 
-This package is _not_ intended to solve every possible concern about Redux, and is deliberately limited in scope. It does _not_ address concepts like "reusable encapsulated Redux modules", data fetching, folder or file structures, managing entity relationships in the store, and so on.
+Because of that, this package is deliberately limited in scope. It does _not_ address concepts like "reusable encapsulated Redux modules", data caching, folder or file structures, managing entity relationships in the store, and so on.
 
 That said, **these tools should be beneficial to all Redux users**. Whether you're a brand new Redux user setting up your
 first project, or an experienced user who wants to simplify an existing application, **Redux Toolkit** can help
@@ -25,12 +25,14 @@ you make your Redux code better.
 
 ## What's Included
 
-Redux Toolkit includes:
+Redux Toolkit includes these APIs:
 
-- A [`configureStore()` function](../api/configureStore.md) with simplified configuration options. It can automatically combine your slice reducers, adds whatever Redux middleware you supply, includes `redux-thunk` by default, and enables use of the Redux DevTools Extension.
-- A [`createReducer()` utility](../api/createReducer.md) that lets you supply a lookup table of action types to case reducer functions, rather than writing switch statements. In addition, it automatically uses the [`immer` library](https://github.com/mweststrate/immer) to let you write simpler immutable updates with normal mutative code, like `state.todos[3].completed = true`.
-- A [`createAction()` utility](../api/createAction.md) that returns an action creator function for the given action type string. The function itself has `toString()` defined, so that it can be used in place of the type constant.
-- A [`createSlice()` function](../api/createSlice.md) that accepts a set of reducer functions, a slice name, and an initial state value, and automatically generates a slice reducer with corresponding action creators and action types.
+- [`configureStore()`](../api/configureStore.md): wraps `createStore` to provide simplified configuration options and good defaults. It can automatically combine your slice reducers, adds whatever Redux middleware you supply, includes `redux-thunk` by default, and enables use of the Redux DevTools Extension.
+- [`createReducer()`](../api/createReducer.md): that lets you supply a lookup table of action types to case reducer functions, rather than writing switch statements. In addition, it automatically uses the [`immer` library](https://github.com/mweststrate/immer) to let you write simpler immutable updates with normal mutative code, like `state.todos[3].completed = true`.
+- [`createAction()`](../api/createAction.md): generates an action creator function for the given action type string. The function itself has `toString()` defined, so that it can be used in place of the type constant.
+- [`createSlice()`](../api/createSlice.md): accepts an object of reducer functions, a slice name, and an initial state value, and automatically generates a slice reducer with corresponding action creators and action types.
+- [`createAsyncThunk`](../api/createAsyncThunk.md): accepts an action type string and a function that returns a promise, and generates a thunk that dispatches `pending/resolved/rejected` action types based on that promise
+- [`createEntityAdapter`](../api/createEntityAdapter.md): generates a set of reusable reducers and selectors to manage normalized data in the store
 - The [`createSelector` utility](../api/createSelector.md) from the [Reselect](https://github.com/reduxjs/reselect) library, re-exported for ease of use.
 
 ## Installation
diff --git a/docs/tutorials/intermediate-tutorial.md b/docs/tutorials/intermediate-tutorial.md
index e98c68f1..7856601a 100644
--- a/docs/tutorials/intermediate-tutorial.md
+++ b/docs/tutorials/intermediate-tutorial.md
@@ -430,10 +430,7 @@ const AddTodo = ({ addTodo }) => {
   )
 }
 
-export default connect(
-  null,
-  mapDispatch
-)(AddTodo)
+export default connect(null, mapDispatch)(AddTodo)
 ```
 
 We start by importing the correct `addTodo` action creator from our todos slice.
diff --git a/docs/usage/usage-guide.md b/docs/usage/usage-guide.md
index c1cf20a9..ff451d03 100644
--- a/docs/usage/usage-guide.md
+++ b/docs/usage/usage-guide.md
@@ -560,10 +560,6 @@ There are many kinds of async middleware for Redux, and each lets you write your
 
 Redux Toolkit does not currently provide any special APIs or syntax for writing thunk functions. In particular, **they cannot be defined as part of a `createSlice()` call**. You have to write them separate from the reducer logic, exactly the same as with plain Redux code.
 
-:::note
-The upcoming [Redux Toolkit v1.3.0 release](https://github.com/reduxjs/redux-toolkit/issues/373), currently in alpha testing, will include [a `createAsyncThunk` API ](https://deploy-preview-374--redux-starter-kit-docs.netlify.com/api/createAsyncThunk) that simplifies the process of writing thunks for async logic.
-:::
-
 Thunks typically dispatch plain actions, such as `dispatch(dataLoaded(response.data))`.
 
 Many Redux apps have structured their code using a "folder-by-type" approach. In that structure, thunk action creators are usually defined in an "actions" file, alongside the plain action creators.
@@ -642,8 +638,65 @@ const fetchIssuesCount = (org, repo) => async dispatch => {
 }
 ```
 
-:::note
-The upcoming [`createAsyncThunk` API in RTK v1.3.0](https://deploy-preview-374--redux-starter-kit-docs.netlify.com/api/createAsyncThunk) will automate this process by generating the action types and dispatching them automatically for you.
-:::
+However, writing code using this approach is tedious. Each separate type of request needs repeated similar implementation:
+
+- Unique action types need to be defined for the three different cases
+- Each of those action types usually has a corresponding action creator function
+- A thunk has to be written that dispatches the correct actions in the right sequence
+
+`createAsyncThunk` abstracts this pattern by generating the action types and action creators and generating a thunk that dispatches those actions.
+
+### Async Requests with `createAsyncThunk`
+
+As a developer, you are probably most concerned with the actual logic needed to make an API request, what action type names show up in the Redux action history log, and how your reducers should process the fetched data. The repetitive details of defining the multiple action types and dispatching the actions in the right sequence aren't what matters.
+
+`createAsyncThunk` simplifies this process - you only need to provide a string for the action type prefix and a payload creator callback that does the actual async logic and returns a promise with the result. In return, `createAsyncThunk` will give you a thunk that will take care of dispatching the right actions based on the promise you return, and action types that you can handle in your reducers:
+
+```js {5-11,22-25,30}
+import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
+import { userAPI } from './userAPI'
+
+// First, create the thunk
+const fetchUserById = createAsyncThunk(
+  'users/fetchByIdStatus',
+  async (userId, thunkAPI) => {
+    const response = await userAPI.fetchById(userId)
+    return response.data
+  }
+)
+
+// Then, handle actions in your reducers:
+const usersSlice = createSlice({
+  name: 'users',
+  initialState: { entities: [], loading: 'idle' },
+  reducers: {
+    // standard reducer logic, with auto-generated action types per reducer
+  },
+  extraReducers: {
+    // Add reducers for additional action types here, and handle loading state as needed
+    [fetchUserById.fulfilled]: (state, action) => {
+      // Add user to the state array
+      state.entities.push(action.payload)
+    }
+  }
+})
+
+// Later, dispatch the thunk as needed in the app
+dispatch(fetchUserById(123))
+```
+
+The thunk action creator accepts a single argument, which will be passed as the first argument to your payload creator callback.
+
+The payload creator will also receive a `thunkAPI` object containing the parameters that are normally passed to a standard Redux thunk function, as well as an auto-generated unique random request ID string and an [`AbortController.signal` object](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal):
+
+```ts
+interface ThunkAPI {
+  dispatch: Function
+  getState: Function
+  extra?: any
+  requestId: string
+  signal: AbortSignal
+}
+```
 
-It's up to you to write reducer logic that handles these action types. We recommend [treating your loading state as a "state machine"](https://redux.js.org/style-guide/style-guide#treat-reducers-as-state-machines) instead of writing boolean flags like `isLoading`.
+You can use any of these as needed inside the payload callback to determine what the final result should be.
diff --git a/docs/usage/usage-with-typescript.md b/docs/usage/usage-with-typescript.md
index 76682e91..0ae3a309 100644
--- a/docs/usage/usage-with-typescript.md
+++ b/docs/usage/usage-with-typescript.md
@@ -22,7 +22,7 @@ Using [configureStore](../api/configureStore.md) should not need any additional
 The easiest way of getting the `State` type is to define the root reducer in advance and extract its `ReturnType`.  
 It is recommend to give the type a different name like `RootState` to prevent confusion, as the type name `State` is usually overused.
 
-```typescript
+```typescript {3}
 import { combineReducers } from '@reduxjs/toolkit'
 const rootReducer = combineReducers({})
 export type RootState = ReturnType<typeof rootReducer>
@@ -33,7 +33,7 @@ export type RootState = ReturnType<typeof rootReducer>
 If you want to get the `Dispatch` type from your store, you can extract it after creating the store.  
 It is recommend to give the type a different name like `AppDispatch` to prevent confusion, as the type name `Dispatch` is usually overused.
 
-```typescript
+```typescript {6}
 import { configureStore } from '@reduxjs/toolkit'
 import rootReducer from './rootReducer'
 const store = configureStore({
@@ -50,7 +50,7 @@ There might however be cases, where TypeScript decides to simplify your provided
 
 Please note that when calling `getDefaultMiddleware` in TypeScript, you have to provide the state type as a generic argument.
 
-```ts
+```ts {10-20}
 import { configureStore } from '@reduxjs/toolkit'
 import additionalMiddleware from 'additional-middleware'
 // @ts-ignore
@@ -110,7 +110,7 @@ createAction('test', withPayloadType<string>())
 
 ### Alternative to using a literally-typed `action.type`
 
-If you are using `action.type` as discriminator on a discriminated union, for example to correctly type your payload in `case` statements, you might be interested in this alternative:
+If you are using `action.type` as a discriminator on a discriminated union, for example to correctly type your payload in `case` statements, you might be interested in this alternative:
 
 Created action creators have a `match` method that acts as a [type predicate](https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates):
 
@@ -128,7 +128,7 @@ This `match` method is also very useful in combination with `redux-observable` a
 
 ## `createReducer`
 
-The default way of calling `createReducer` would be with a map object, like this:
+The default way of calling `createReducer` would be with a "lookup table" / "map object", like this:
 
 ```typescript
 createReducer(0, {
@@ -159,7 +159,7 @@ As an alternative, RTK includes a type-safe reducer builder API.
 
 Instead of using a simple object as an argument to `createReducer`, you can also use a callback that receives a `ActionReducerMapBuilder` instance:
 
-```typescript
+```typescript {3-10}
 const increment = createAction<number, 'increment'>('increment')
 const decrement = createAction<number, 'decrement'>('decrement')
 createReducer(0, builder =>
@@ -213,33 +213,68 @@ createSlice({
 })
 ```
 
-### Defining the type of your `initialState`
+### Defining the Initial State Type
 
 You might have noticed that it is not a good idea to pass your `SliceState` type as a generic to `createSlice`. This is due to the fact that in almost all cases, follow-up generic parameters to `createSlice` need to be inferred, and TypeScript cannot mix explicit declaration and inference of generic types within the same "generic block".
 
-Instead, you can use the construct `initialState: myInitialState as SliceState`.
+The standard approach is to declare an interface or type for your state, create an initial state value that uses that type, and pass the initial state value to `createSlice. You can also use the construct`initialState: myInitialState as SliceState`.
 
-```ts
+```ts {1,4,8,15}
 type SliceState = { state: 'loading' } | { state: 'finished'; data: string }
 
+// First approach: define the initial state using that type
+const initialState: SliceState = { state: 'loading' }
+
 createSlice({
-  name: 'test',
+  name: 'test1',
+  initialState, // type SliceState is inferred for the state of the slice
+  reducers: {}
+})
+
+// Or, cast the initial state as necessary
+createSlice({
+  name: 'test2',
   initialState: { state: 'loading' } as SliceState,
-  reducers: {
-    // ...
-  }
+  reducers: {}
 })
 ```
 
 which will result in a `Slice<SliceState, ...>`.
 
-### On the "type" property of slice action Reducers
+### Defining Action Contents with `prepare` Callbacks
 
-As TS cannot combine two string literals (`slice.name` and the key of `actionMap`) into a new literal, all actionCreators created by createSlice are of type 'string'. This is usually not a problem, as these types are only rarely used as literals.
+If you want to add a `meta` or `error` property to your action, or customize the `payload` of your action, you have to use the `prepare` notation.
 
-In most cases that type would be required as a literal, the `slice.action.myAction.match` [type predicate](https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates) should prove as a viable alternative:
+Using this notation with TypeScript looks like this:
 
-```typescript
+```ts {5-16}
+const blogSlice = createSlice({
+  name: 'blogData',
+  initialState,
+  reducers: {
+    receivedAll: {
+      reducer(
+        state,
+        action: PayloadAction<Page[], string, { currentPage: number }>
+      ) {
+        state.all = action.payload
+        state.meta = action.meta
+      },
+      prepare(payload: Page[], currentPage: number) {
+        return { payload, meta: { currentPage } }
+      }
+    }
+  }
+})
+```
+
+### Generated Action Types for Slices
+
+As TS cannot combine two string literals (`slice.name` and the key of `actionMap`) into a new literal, all actionCreators created by `createSlice` are of type 'string'. This is usually not a problem, as these types are only rarely used as literals.
+
+In most cases that `type` would be required as a literal, the `slice.action.myAction.match` [type predicate](https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates) should be a viable alternative:
+
+```ts {10}
 const slice = createSlice({
   name: 'test',
   initialState: 0,
@@ -259,7 +294,44 @@ If you actually _need_ that type, unfortunately there is no other way than manua
 
 ### Type safety with `extraReducers`
 
-Like in `createReducer`, the `extraReducers` map object is not easy to fully type. So, like with `createReducer`, you may also use the "builder callback" approach for defining the reducer object argument. See [the `createReducer` section above](#createreducer) for an example.
+Reducer lookup tables that map an action `type` string to a reducer function are not easy to fully type correctly. This affects both `createReducer` and the `extraReducers` argument for `createSlice`. So, like with `createReducer`, [you may also use the "builder callback" approach](#building-type-safe-reducer-argument-objects) for defining the reducer object argument.
+
+This is particularly useful when a slice reducer needs to handle action types generated by other slices, or generated by specific calls to `createAction` (such as the actions generated by [`createAsyncThunk`](../api/createAsyncThunk.md)).
+
+```ts {27-30}
+const fetchUserById = createAsyncThunk(
+  'users/fetchById',
+  // if you type your function argument here
+  async (userId: number) => {
+    const response = await fetch(`https://reqres.in/api/users/${userId}`)
+    return (await response.json()) as Returned
+  }
+)
+
+interface UsersState {
+  entities: []
+  loading: 'idle' | 'pending' | 'succeeded' | 'failed'
+}
+
+const initialState: UsersState = {
+  entities: [],
+  loading: 'idle'
+}
+
+const usersSlice = createSlice({
+  name: 'users',
+  initialState,
+  reducers: {
+    // fill in primary logic here
+  },
+  extraReducers: builder => {
+    builder.addCase(fetchUserById.pending, (state, action) => {
+      // both `state` and `action` are now correctly typed
+      // based on the slice state and the `pending` action creator
+    })
+  }
+})
+```
 
 ### Wrapping `createSlice`
 
@@ -319,3 +391,181 @@ const wrappedSlice = createGenericSlice({
   }
 })
 ```
+
+## `createAsyncThunk`
+
+In the most common use cases, you should not need to explicitly declare any types for the `createAsyncThunk` call itself.
+
+Just provide a type for the first argument to the `payloadCreator` argument as you would for any function argument, and the resulting thunk will accept the same type as its input parameter.
+The return type of the `payloadCreator` will also be reflected in all generated action types.
+
+```ts {8,11,18}
+interface MyData {
+  // ...
+}
+
+const fetchUserById = createAsyncThunk(
+  'users/fetchById',
+  // Declare the type your function argument here:
+  async (userId: number) => {
+    const response = await fetch(`https://reqres.in/api/users/${userId}`)
+    // Inferred return type: Promise<MyData>
+    return (await response.json()) as MyData
+  }
+)
+
+// the parameter of `fetchUserById` is automatically inferred to `number` here
+// and dispatching the resulting thunkAction will return a Promise of a correctly
+// typed "fulfilled" or "rejected" action.
+const lastReturnedAction = await store.dispatch(fetchUserById(3))
+```
+
+The second argument to the `payloadCreator`, known as `thunkApi`, is an object containing references to the `dispatch`, `getState`, and `extra` arguments from the thunk middleware as well as a utility function called `rejectWithValue`. If you want to use these from within the `payloadCreator`, you will need to define some generic arguments, as the types for these arguments cannot be inferred. Also, as TS cannot mix explicit and inferred generic parameters, from this point on you'll have to define the `Returned` and `ThunkArg` generic parameter as well.
+
+To define the types for these arguments, pass an object as the third generic argument, with type declarations for some or all of these fields: `{dispatch?, state?, extra?, rejectValue?}`.
+
+```ts
+const fetchUserById = createAsyncThunk<
+  // Return type of the payload creator
+  MyData,
+  // First argument to the payload creator
+  number,
+  {
+    dispatch: AppDispatch
+    state: State
+    extra: {
+      jwt: string
+    }
+  }
+>('users/fetchById', async (userId, thunkApi) => {
+  const response = await fetch(`https://reqres.in/api/users/${userId}`, {
+    headers: {
+      Authorization: `Bearer ${thunkApi.extra.jwt}`
+    }
+  })
+  return (await response.json()) as MyData
+})
+```
+
+If you are performing a request that you know will typically either be a success or have an expected error format, you can pass in a type to `rejectValue` and `return rejectWithValue(knownPayload)` in the action creator. This allows you to reference the error payload in the reducer as well as in a component after dispatching the `createAsyncThunk` action.
+
+```ts
+interface MyKnownError {
+  errorMessage: string
+  // ...
+}
+interface UserAttributes {
+  id: string
+  first_name: string
+  last_name: string
+  email: string
+}
+
+const updateUser = createAsyncThunk<
+  // Return type of the payload creator
+  MyData,
+  // First argument to the payload creator
+  UserAttributes,
+  // Types for ThunkAPI
+  {
+    extra: {
+      jwt: string
+    }
+    rejectValue: MyKnownError
+  }
+>('users/update', async (user, thunkApi) => {
+  const { id, ...userData } = user
+  const response = await fetch(`https://reqres.in/api/users/${id}`, {
+    method: 'PUT',
+    headers: {
+      Authorization: `Bearer ${thunkApi.extra.jwt}`
+    },
+    body: JSON.stringify(userData)
+  })
+  if (response.status === 400) {
+    // Return the known error for future handling
+    return thunkApi.rejectWithValue((await response.json()) as MyKnownError)
+  }
+  return (await response.json()) as MyData
+})
+```
+
+While this notation for `state`, `dispatch`, `extra` and `rejectValue` might seem uncommon at first, it allows you to provide only the types for these you actually need - so for example, if you are not accessing `getState` within your `payloadCreator`, there is no need to provide a type for `state`. The same can be said about `rejectValue` - if you don't need to access any potential error payload, you can ignore it.
+
+In addition, you can leverage checks against `action.payload` and `match` as provided by `createAction` as a type-guard for when you want to access known properties on defined types. Example:
+
+- In a reducer
+
+```ts
+const usersSlice = createSlice({
+  name: 'users',
+  initialState: {
+    entities: {},
+    error: null
+  },
+  reducers: {},
+  extraReducers: builder => {
+    builder.addCase(updateUser.fulfilled, (state, { payload }) => {
+      state.entities[payload.id] = payload
+    })
+    builder.addCase(updateUser.rejected, (state, action) => {
+      if (action.payload) {
+        // Since we passed in `MyKnownError` to `rejectType` in `updateUser`, the type information will be available here.
+        state.error = action.payload.errorMessage
+      } else {
+        state.error = action.error
+      }
+    })
+  }
+})
+```
+
+- In a component
+
+```ts
+const handleUpdateUser = async userData => {
+  const resultAction = await dispatch(updateUser(userData))
+  if (updateUser.fulfilled.match(resultAction)) {
+    const user = unwrapResult(resultAction)
+    showToast('success', `Updated ${user.name}`)
+  } else {
+    if (resultAction.payload) {
+      // Since we passed in `MyKnownError` to `rejectType` in `updateUser`, the type information will be available here.
+      // Note: this would also be a good place to do any handling that relies on the `rejectedWithValue` payload, such as setting field errors
+      showToast('error', `Update failed: ${resultAction.payload.errorMessage}`)
+    } else {
+      showToast('error', `Update failed: ${resultAction.error.message}`)
+    }
+  }
+}
+```
+
+## `createEntityAdapter`
+
+Typing `createEntityAdapter` only requires you to specify the entity type as the single generic argument.
+
+The example from the `createEntityAdapter` documentation would look like this in TypeScript:
+
+```ts {7}
+interface Book {
+  bookId: number
+  title: string
+  // ...
+}
+
+const booksAdapter = createEntityAdapter<Book>({
+  selectId: book => book.bookId,
+  sortComparer: (a, b) => a.title.localeCompare(b.title)
+})
+
+const booksSlice = createSlice({
+  name: 'books',
+  initialState: booksAdapter.getInitialState(),
+  reducers: {
+    bookAdded: booksAdapter.addOne,
+    booksReceived(state, action: PayloadAction<{ books: Book[] }>) {
+      booksAdapter.setAll(state, action.payload.books)
+    }
+  }
+})
+```
diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md
index f435fb8e..c3272732 100644
--- a/etc/redux-toolkit.api.md
+++ b/etc/redux-toolkit.api.md
@@ -5,19 +5,24 @@
 ```ts
 
 import { Action } from 'redux';
+import { ActionCreator } from 'redux';
 import { AnyAction } from 'redux';
 import { default as createNextState } from 'immer';
 import { createSelector } from 'reselect';
 import { DeepPartial } from 'redux';
 import { Dispatch } from 'redux';
 import { Draft } from 'immer';
-import { EnhancerOptions } from 'redux-devtools-extension';
 import { Middleware } from 'redux';
+import { OutputParametricSelector } from 'reselect';
+import { OutputSelector } from 'reselect';
+import { ParametricSelector } from 'reselect';
 import { Reducer } from 'redux';
 import { ReducersMapObject } from 'redux';
+import { Selector } from 'reselect';
 import { Store } from 'redux';
 import { StoreEnhancer } from 'redux';
 import { ThunkAction } from 'redux-thunk';
+import { ThunkDispatch } from 'redux-thunk';
 import { ThunkMiddleware } from 'redux-thunk';
 
 // @public
@@ -27,8 +32,7 @@ export interface ActionCreatorWithNonInferrablePayload<T extends string = string
 
 // @public
 export interface ActionCreatorWithOptionalPayload<P, T extends string = string> extends BaseActionCreator<P, T> {
-    (payload?: undefined): PayloadAction<undefined, T>;
-    <PT extends Diff<P, undefined>>(payload?: PT): PayloadAction<PT, T>;
+    (payload?: P): PayloadAction<P, T>;
 }
 
 // @public
@@ -38,7 +42,6 @@ export interface ActionCreatorWithoutPayload<T extends string = string> extends
 
 // @public
 export interface ActionCreatorWithPayload<P, T extends string = string> extends BaseActionCreator<P, T> {
-    <PT extends P>(payload: PT): PayloadAction<PT, T>;
     (payload: P): PayloadAction<P, T>;
 }
 
@@ -77,6 +80,9 @@ export type CaseReducerWithPrepare<State, Action extends PayloadAction> = {
     prepare: PrepareAction<Action['payload']>;
 };
 
+// @alpha (undocumented)
+export type Comparer<T> = (a: T, b: T) => number;
+
 // @public
 export type ConfigureEnhancersCallback = (defaultEnhancers: StoreEnhancer[]) => StoreEnhancer[];
 
@@ -98,6 +104,38 @@ export function createAction<P = void, T extends string = string>(type: T): Payl
 // @public
 export function createAction<PA extends PrepareAction<any>, T extends string = string>(type: T, prepareAction: PA): PayloadActionCreator<ReturnType<PA>['payload'], T, PA>;
 
+// @alpha (undocumented)
+export function createAsyncThunk<Returned, ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {}>(type: string, payloadCreator: (arg: ThunkArg, thunkAPI: GetThunkAPI<ThunkApiConfig>) => Promise<Returned | RejectWithValue<GetRejectValue<ThunkApiConfig>>> | Returned | RejectWithValue<GetRejectValue<ThunkApiConfig>>): ((arg: ThunkArg) => (dispatch: GetDispatch<ThunkApiConfig>, getState: () => GetState<ThunkApiConfig>, extra: GetExtra<ThunkApiConfig>) => Promise<PayloadAction<Returned, string, {
+    arg: ThunkArg;
+    requestId: string;
+}, never> | PayloadAction<GetRejectValue<ThunkApiConfig> | undefined, string, {
+    arg: ThunkArg;
+    requestId: string;
+    aborted: boolean;
+}, SerializedError>> & {
+    abort: (reason?: string | undefined) => void;
+}) & {
+    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;
+    }>;
+    fulfilled: ActionCreatorWithPreparedPayload<[Returned, string, ThunkArg], Returned, string, never, {
+        arg: ThunkArg;
+        requestId: string;
+    }>;
+};
+
+// @alpha (undocumented)
+export function createEntityAdapter<T>(options?: {
+    selectId?: IdSelector<T>;
+    sortComparer?: false | Comparer<T>;
+}): EntityAdapter<T>;
+
 export { createNextState }
 
 // @public
@@ -122,6 +160,12 @@ export interface CreateSliceOptions<State = any, CR extends SliceCaseReducers<St
     reducers: ValidateSliceCaseReducers<State, CR>;
 }
 
+// @alpha (undocumented)
+export interface Dictionary<T> extends DictionaryNum<T> {
+    // (undocumented)
+    [id: string]: T | undefined;
+}
+
 export { Draft }
 
 // @public
@@ -129,6 +173,30 @@ export interface EnhancedStore<S = any, A extends Action = AnyAction, M extends
     dispatch: DispatchForMiddlewares<M> & Dispatch<A>;
 }
 
+// @alpha (undocumented)
+export interface EntityAdapter<T> extends EntityStateAdapter<T> {
+    // (undocumented)
+    getInitialState(): EntityState<T>;
+    // (undocumented)
+    getInitialState<S extends object>(state: S): EntityState<T> & S;
+    // (undocumented)
+    getSelectors(): EntitySelectors<T, EntityState<T>>;
+    // (undocumented)
+    getSelectors<V>(selectState: (state: V) => EntityState<T>): EntitySelectors<T, V>;
+    // (undocumented)
+    selectId: IdSelector<T>;
+    // (undocumented)
+    sortComparer: false | Comparer<T>;
+}
+
+// @alpha (undocumented)
+export interface EntityState<T> {
+    // (undocumented)
+    entities: Dictionary<T>;
+    // (undocumented)
+    ids: EntityId[];
+}
+
 // @public (undocumented)
 export function findNonSerializableValue(value: unknown, path?: ReadonlyArray<string>, isSerializable?: (value: unknown) => boolean, getEntries?: (value: unknown) => [string, any][], ignoredPaths?: string[]): NonSerializableValue | false;
 
@@ -142,9 +210,18 @@ export function getDefaultMiddleware<S = any, O extends Partial<GetDefaultMiddle
 // @public
 export function getType<T extends string>(actionCreator: PayloadActionCreator<any, T>): T;
 
+// @alpha (undocumented)
+export type IdSelector<T> = (model: T) => EntityId;
+
 // @public
 export function isPlain(val: any): boolean;
 
+export { OutputParametricSelector }
+
+export { OutputSelector }
+
+export { ParametricSelector }
+
 // @public
 export type PayloadAction<P = void, T extends string = string, M = never, E = never> = {
     payload: P;
@@ -173,12 +250,27 @@ export type PrepareAction<P> = ((...args: any[]) => {
     error: any;
 });
 
+export { Selector }
+
 // @public
 export interface SerializableStateInvariantMiddlewareOptions {
     getEntries?: (value: any) => [string, any][];
     ignoredActions?: string[];
     ignoredPaths?: string[];
     isSerializable?: (value: any) => boolean;
+    warnAfter?: number;
+}
+
+// @alpha (undocumented)
+export interface SerializedError {
+    // (undocumented)
+    code?: string;
+    // (undocumented)
+    message?: string;
+    // (undocumented)
+    name?: string;
+    // (undocumented)
+    stack?: string;
 }
 
 // @public
@@ -199,6 +291,15 @@ export type SliceCaseReducers<State> = {
 
 export { ThunkAction }
 
+// @alpha (undocumented)
+export function unwrapResult<R extends ActionTypesWithOptionalErrorAction>(returned: R): PayloadForActionTypesExcludingErrorActions<R>;
+
+// @alpha (undocumented)
+export type Update<T> = {
+    id: EntityId;
+    changes: Partial<T>;
+};
+
 // @public
 export type ValidateSliceCaseReducers<S, ACR extends SliceCaseReducers<S>> = ACR & {
     [T in keyof ACR]: ACR[T] extends {
diff --git a/package-lock.json b/package-lock.json
index 1b76d048..6fbf05b5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "@reduxjs/toolkit",
-  "version": "1.2.5",
+  "version": "1.3.0-beta.1",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -1324,12 +1324,27 @@
       "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==",
       "dev": true
     },
+    "@types/json-stringify-safe": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/@types/json-stringify-safe/-/json-stringify-safe-5.0.0.tgz",
+      "integrity": "sha512-UUA1sH0RSRROdInuDOA1yoRzbi5xVFD1RHCoOvNRPTNwR8zBkJ/84PZ6NhKVDtKp0FTeIccJCdQz1X2aJPr4uw==",
+      "dev": true
+    },
     "@types/minimatch": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
       "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
       "dev": true
     },
+    "@types/nanoid": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/@types/nanoid/-/nanoid-2.1.0.tgz",
+      "integrity": "sha512-xdkn/oRTA0GSNPLIKZgHWqDTWZsVrieKomxJBOQUK9YDD+zfSgmwD5t4WJYra5S7XyhTw7tfvwznW+pFexaepQ==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "@types/node": {
       "version": "10.14.6",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.6.tgz",
@@ -1342,15 +1357,6 @@
       "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
       "dev": true
     },
-    "@types/redux-immutable-state-invariant": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/@types/redux-immutable-state-invariant/-/redux-immutable-state-invariant-2.1.1.tgz",
-      "integrity": "sha512-eosvcaKraeX8fyEKViFZ2vh3xSWF8teCSw+mEMjlwoN0Ai6ZAevczQr3YyZxZFfwH1LXUiV3aXcSAlsNKAnjNg==",
-      "dev": true,
-      "requires": {
-        "redux": "^3.6.0 || ^4.0.0"
-      }
-    },
     "@types/resolve": {
       "version": "0.0.8",
       "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
@@ -1682,6 +1688,15 @@
       "integrity": "sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==",
       "dev": true
     },
+    "axios": {
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
+      "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
+      "dev": true,
+      "requires": {
+        "follow-redirects": "1.5.10"
+      }
+    },
     "axobject-query": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.1.1.tgz",
@@ -3612,6 +3627,32 @@
       "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==",
       "dev": true
     },
+    "follow-redirects": {
+      "version": "1.5.10",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
+      "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
+      "dev": true,
+      "requires": {
+        "debug": "=3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
     "for-in": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -3674,25 +3715,29 @@
       "dependencies": {
         "abbrev": {
           "version": "1.1.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
           "dev": true,
           "optional": true
         },
         "ansi-regex": {
           "version": "2.1.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
           "dev": true,
           "optional": true
         },
         "aproba": {
           "version": "1.2.0",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
           "dev": true,
           "optional": true
         },
         "are-we-there-yet": {
           "version": "1.1.5",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3702,13 +3747,15 @@
         },
         "balanced-match": {
           "version": "1.0.0",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
           "dev": true,
           "optional": true
         },
         "brace-expansion": {
           "version": "1.1.11",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3718,37 +3765,43 @@
         },
         "chownr": {
           "version": "1.1.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==",
           "dev": true,
           "optional": true
         },
         "code-point-at": {
           "version": "1.1.0",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
           "dev": true,
           "optional": true
         },
         "concat-map": {
           "version": "0.0.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
           "dev": true,
           "optional": true
         },
         "console-control-strings": {
           "version": "1.1.0",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
           "dev": true,
           "optional": true
         },
         "core-util-is": {
           "version": "1.0.2",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
           "dev": true,
           "optional": true
         },
         "debug": {
           "version": "4.1.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3757,25 +3810,29 @@
         },
         "deep-extend": {
           "version": "0.6.0",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
           "dev": true,
           "optional": true
         },
         "delegates": {
           "version": "1.0.0",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
           "dev": true,
           "optional": true
         },
         "detect-libc": {
           "version": "1.0.3",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
           "dev": true,
           "optional": true
         },
         "fs-minipass": {
           "version": "1.2.5",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3784,13 +3841,15 @@
         },
         "fs.realpath": {
           "version": "1.0.0",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
           "dev": true,
           "optional": true
         },
         "gauge": {
           "version": "2.7.4",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3806,7 +3865,8 @@
         },
         "glob": {
           "version": "7.1.3",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3820,13 +3880,15 @@
         },
         "has-unicode": {
           "version": "2.0.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
           "dev": true,
           "optional": true
         },
         "iconv-lite": {
           "version": "0.4.24",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3835,7 +3897,8 @@
         },
         "ignore-walk": {
           "version": "3.0.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3844,7 +3907,8 @@
         },
         "inflight": {
           "version": "1.0.6",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3854,19 +3918,22 @@
         },
         "inherits": {
           "version": "2.0.3",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
           "dev": true,
           "optional": true
         },
         "ini": {
           "version": "1.3.5",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
           "dev": true,
           "optional": true
         },
         "is-fullwidth-code-point": {
           "version": "1.0.0",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3875,13 +3942,15 @@
         },
         "isarray": {
           "version": "1.0.0",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
           "dev": true,
           "optional": true
         },
         "minimatch": {
           "version": "3.0.4",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3890,13 +3959,15 @@
         },
         "minimist": {
           "version": "0.0.8",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
           "dev": true,
           "optional": true
         },
         "minipass": {
           "version": "2.3.5",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3906,7 +3977,8 @@
         },
         "minizlib": {
           "version": "1.2.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3915,7 +3987,8 @@
         },
         "mkdirp": {
           "version": "0.5.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3924,13 +3997,15 @@
         },
         "ms": {
           "version": "2.1.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
           "dev": true,
           "optional": true
         },
         "needle": {
           "version": "2.3.0",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3941,7 +4016,8 @@
         },
         "node-pre-gyp": {
           "version": "0.12.0",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3959,7 +4035,8 @@
         },
         "nopt": {
           "version": "4.0.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3969,13 +4046,15 @@
         },
         "npm-bundled": {
           "version": "1.0.6",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==",
           "dev": true,
           "optional": true
         },
         "npm-packlist": {
           "version": "1.4.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3985,7 +4064,8 @@
         },
         "npmlog": {
           "version": "4.1.2",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -3997,19 +4077,22 @@
         },
         "number-is-nan": {
           "version": "1.0.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
           "dev": true,
           "optional": true
         },
         "object-assign": {
           "version": "4.1.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
           "dev": true,
           "optional": true
         },
         "once": {
           "version": "1.4.0",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
           "dev": true,
           "optional": true,
           "requires": {
@@ -4018,19 +4101,22 @@
         },
         "os-homedir": {
           "version": "1.0.2",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
           "dev": true,
           "optional": true
         },
         "os-tmpdir": {
           "version": "1.0.2",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
           "dev": true,
           "optional": true
         },
         "osenv": {
           "version": "0.1.5",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -4040,19 +4126,22 @@
         },
         "path-is-absolute": {
           "version": "1.0.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
           "dev": true,
           "optional": true
         },
         "process-nextick-args": {
           "version": "2.0.0",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
           "dev": true,
           "optional": true
         },
         "rc": {
           "version": "1.2.8",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -4064,7 +4153,8 @@
           "dependencies": {
             "minimist": {
               "version": "1.2.0",
-              "bundled": true,
+              "resolved": false,
+              "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
               "dev": true,
               "optional": true
             }
@@ -4072,7 +4162,8 @@
         },
         "readable-stream": {
           "version": "2.3.6",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -4087,7 +4178,8 @@
         },
         "rimraf": {
           "version": "2.6.3",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -4096,43 +4188,50 @@
         },
         "safe-buffer": {
           "version": "5.1.2",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
           "dev": true,
           "optional": true
         },
         "safer-buffer": {
           "version": "2.1.2",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
           "dev": true,
           "optional": true
         },
         "sax": {
           "version": "1.2.4",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
           "dev": true,
           "optional": true
         },
         "semver": {
           "version": "5.7.0",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
           "dev": true,
           "optional": true
         },
         "set-blocking": {
           "version": "2.0.0",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
           "dev": true,
           "optional": true
         },
         "signal-exit": {
           "version": "3.0.2",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
           "dev": true,
           "optional": true
         },
         "string-width": {
           "version": "1.0.2",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
           "dev": true,
           "optional": true,
           "requires": {
@@ -4143,7 +4242,8 @@
         },
         "string_decoder": {
           "version": "1.1.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -4152,7 +4252,8 @@
         },
         "strip-ansi": {
           "version": "3.0.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
           "dev": true,
           "optional": true,
           "requires": {
@@ -4161,13 +4262,15 @@
         },
         "strip-json-comments": {
           "version": "2.0.1",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
           "dev": true,
           "optional": true
         },
         "tar": {
           "version": "4.4.8",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -4182,13 +4285,15 @@
         },
         "util-deprecate": {
           "version": "1.0.2",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
           "dev": true,
           "optional": true
         },
         "wide-align": {
           "version": "1.1.3",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
           "dev": true,
           "optional": true,
           "requires": {
@@ -4197,13 +4302,15 @@
         },
         "wrappy": {
           "version": "1.0.2",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
           "dev": true,
           "optional": true
         },
         "yallist": {
           "version": "3.0.3",
-          "bundled": true,
+          "resolved": false,
+          "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
           "dev": true,
           "optional": true
         }
@@ -4456,9 +4563,9 @@
       "dev": true
     },
     "immer": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/immer/-/immer-4.0.1.tgz",
-      "integrity": "sha512-qZFpWApnbubcJ03gvWa6zUQz2OTkDc6yINdFXI1lm2IoMYsiVVMrxvfeLDURFhUGJfsQysDUjHSXYnJfMMHAwQ=="
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-6.0.1.tgz",
+      "integrity": "sha512-oXwigCKgznQywsXi1VgrqgWbQEU3wievNCVc4Fcwky6mwXU6YHj6JuYp0WEM/B1EphkqsLr0x18lm5OiuemPcA=="
     },
     "import-fresh": {
       "version": "3.2.1",
@@ -4604,6 +4711,7 @@
       "version": "2.2.4",
       "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
       "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+      "dev": true,
       "requires": {
         "loose-envify": "^1.0.0"
       }
@@ -5641,7 +5749,8 @@
     "json-stringify-safe": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
-      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
+      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
+      "dev": true
     },
     "json5": {
       "version": "2.1.1",
@@ -6652,9 +6761,9 @@
       "dev": true
     },
     "prettier": {
-      "version": "1.18.2",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz",
-      "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==",
+      "version": "1.19.1",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
+      "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
       "dev": true
     },
     "prettier-linter-helpers": {
@@ -6844,20 +6953,6 @@
         "symbol-observable": "^1.2.0"
       }
     },
-    "redux-devtools-extension": {
-      "version": "2.13.8",
-      "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz",
-      "integrity": "sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg=="
-    },
-    "redux-immutable-state-invariant": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/redux-immutable-state-invariant/-/redux-immutable-state-invariant-2.1.0.tgz",
-      "integrity": "sha512-3czbDKs35FwiBRsx/3KabUk5zSOoTXC+cgVofGkpBNv3jQcqIe5JrHcF5AmVt7B/4hyJ8MijBIpCJ8cife6yJg==",
-      "requires": {
-        "invariant": "^2.1.0",
-        "json-stringify-safe": "^5.0.1"
-      }
-    },
     "redux-thunk": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
@@ -8444,9 +8539,9 @@
       "dev": true
     },
     "typescript": {
-      "version": "3.6.3",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz",
-      "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==",
+      "version": "3.8.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.2.tgz",
+      "integrity": "sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ==",
       "dev": true
     },
     "typings-tester": {
diff --git a/package.json b/package.json
index ca39e00a..8ddd5ac2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@reduxjs/toolkit",
-  "version": "1.2.5",
+  "version": "1.3.0-beta.1",
   "description": "The official, opinionated, batteries-included toolset for efficient Redux development",
   "repository": "https://github.com/reduxjs/redux-toolkit",
   "keywords": [
@@ -25,24 +25,28 @@
   "devDependencies": {
     "@microsoft/api-extractor": "^7.7.0",
     "@types/jest": "^24.0.11",
+    "@types/json-stringify-safe": "^5.0.0",
+    "@types/nanoid": "^2.1.0",
     "@types/node": "^10.14.4",
-    "@types/redux-immutable-state-invariant": "^2.1.1",
+    "axios": "^0.19.2",
     "console-testing-library": "^0.3.1",
     "eslint-config-react-app": "^5.0.1",
-    "prettier": "^1.18.2",
+    "invariant": "^2.2.4",
+    "json-stringify-safe": "^5.0.1",
+    "prettier": "^1.19.1",
     "react": "^16.8.6",
     "rollup-plugin-strip-code": "^0.2.6",
     "tsdx": "^0.11.0",
     "tslib": "^1.10.0",
-    "typescript": "^3.6.3",
+    "typescript": "^3.8.2",
     "typings-tester": "^0.3.2"
   },
   "scripts": {
     "build-ci": "tsdx build --format cjs,esm,umd --name redux-toolkit && api-extractor run",
     "build": "tsdx build --format cjs,esm,umd --name redux-toolkit && api-extractor run --local",
     "dev": "tsdx watch  --format cjs,esm,umd",
-    "format": "prettier --write \"src/*.ts\" \"**/*.md\"",
-    "format:check": "prettier --list-different \"src/*.ts\" \"**/*.md\"",
+    "format": "prettier --write \"src/**/*.ts\" \"**/*.md\"",
+    "format:check": "prettier --list-different \"src/**/*.ts\" \"docs/*/**.md\"",
     "lint": "tsdx lint src",
     "prepare": "npm run lint && npm run format:check && npm test && npm run build-ci",
     "test": "tsdx test"
@@ -54,10 +58,8 @@
     "src"
   ],
   "dependencies": {
-    "immer": "^4.0.1",
+    "immer": "^6.0.1",
     "redux": "^4.0.0",
-    "redux-devtools-extension": "^2.13.8",
-    "redux-immutable-state-invariant": "^2.1.0",
     "redux-thunk": "^2.3.0",
     "reselect": "^4.0.0"
   },
diff --git a/src/configureStore.ts b/src/configureStore.ts
index f0078f61..907641be 100644
--- a/src/configureStore.ts
+++ b/src/configureStore.ts
@@ -16,7 +16,7 @@ import {
 import {
   composeWithDevTools,
   EnhancerOptions as DevToolsOptions
-} from 'redux-devtools-extension'
+} from './devtoolsExtension'
 
 import isPlainObject from './isPlainObject'
 import {
diff --git a/src/createAction.ts b/src/createAction.ts
index 73c32ef0..93558d7b 100644
--- a/src/createAction.ts
+++ b/src/createAction.ts
@@ -5,6 +5,7 @@ import {
   IfVoid,
   IsAny
 } from './tsHelpers'
+import isPlainObject from './isPlainObject'
 
 /**
  * An action with a string type and an associated payload. This is the
@@ -122,16 +123,12 @@ export interface ActionCreatorWithPreparedPayload<
  */
 export interface ActionCreatorWithOptionalPayload<P, T extends string = string>
   extends BaseActionCreator<P, T> {
-  /**
-   * Calling this {@link redux#ActionCreator} without arguments will
-   * return a {@link PayloadAction} of type `T` with a payload of `undefined`
-   */
-  (payload?: undefined): PayloadAction<undefined, T>
   /**
    * Calling this {@link redux#ActionCreator} with an argument will
-   * return a {@link PayloadAction} of type `T` with a payload of `P`
+   * return a {@link PayloadAction} of type `T` with a payload of `P`.
+   * Calling it without an argument will return a PayloadAction with a payload of `undefined`.
    */
-  <PT extends Diff<P, undefined>>(payload?: PT): PayloadAction<PT, T>
+  (payload?: P): PayloadAction<P, T>
 }
 
 /**
@@ -159,12 +156,6 @@ export interface ActionCreatorWithoutPayload<T extends string = string>
  */
 export interface ActionCreatorWithPayload<P, T extends string = string>
   extends BaseActionCreator<P, T> {
-  /**
-   * Calling this {@link redux#ActionCreator} with an argument will
-   * return a {@link PayloadAction} of type `T` with a payload of `P`
-   * If possible, `P` will be narrowed down to the exact type of the payload argument.
-   */
-  <PT extends P>(payload: PT): PayloadAction<PT, T>
   /**
    * Calling this {@link redux#ActionCreator} with an argument will
    * return a {@link PayloadAction} of type `T` with a payload of `P`
@@ -295,6 +286,25 @@ export function createAction(type: string, prepareAction?: Function): any {
   return actionCreator
 }
 
+export function isFSA(
+  action: unknown
+): action is {
+  type: string
+  payload?: unknown
+  error?: unknown
+  meta?: unknown
+} {
+  return (
+    isPlainObject(action) &&
+    typeof (action as any).type === 'string' &&
+    Object.keys(action).every(isValidKey)
+  )
+}
+
+function isValidKey(key: string) {
+  return ['type', 'payload', 'error', 'meta'].indexOf(key) > -1
+}
+
 /**
  * Returns the action type of the actions created by the passed
  * `createAction()`-generated action creator (arbitrary action creators
@@ -313,8 +323,6 @@ export function getType<T extends string>(
 
 // helper types for more readable typings
 
-type Diff<T, U> = T extends U ? never : T
-
 type IfPrepareActionMethodProvided<
   PA extends PrepareAction<any> | void,
   True,
diff --git a/src/createAsyncThunk.ts b/src/createAsyncThunk.ts
new file mode 100644
index 00000000..f7f2f138
--- /dev/null
+++ b/src/createAsyncThunk.ts
@@ -0,0 +1,285 @@
+import { Dispatch, AnyAction } from 'redux'
+import {
+  createAction,
+  PayloadAction,
+  ActionCreatorWithPreparedPayload
+} from './createAction'
+import { ThunkDispatch } from 'redux-thunk'
+import { FallbackIfUnknown } from './tsHelpers'
+import { nanoid } from './nanoid'
+
+// @ts-ignore we need the import of these types due to a bundling issue.
+type _Keep = PayloadAction | ActionCreatorWithPreparedPayload<any, unknown>
+
+export type BaseThunkAPI<
+  S,
+  E,
+  D extends Dispatch = Dispatch,
+  RejectedValue = undefined
+> = {
+  dispatch: D
+  getState: () => S
+  extra: E
+  requestId: string
+  signal: AbortSignal
+  rejectWithValue(value: RejectedValue): RejectWithValue<RejectedValue>
+}
+
+/**
+ * @alpha
+ */
+export interface SerializedError {
+  name?: string
+  message?: string
+  stack?: string
+  code?: string
+}
+
+const commonProperties: Array<keyof SerializedError> = [
+  'name',
+  'message',
+  'stack',
+  'code'
+]
+
+class RejectWithValue<RejectValue> {
+  constructor(public readonly value: RejectValue) {}
+}
+
+// Reworked from https://github.com/sindresorhus/serialize-error
+export const miniSerializeError = (value: any): SerializedError => {
+  if (typeof value === 'object' && value !== null) {
+    const simpleError: SerializedError = {}
+    for (const property of commonProperties) {
+      if (typeof value[property] === 'string') {
+        simpleError[property] = value[property]
+      }
+    }
+
+    return simpleError
+  }
+
+  return { message: String(value) }
+}
+
+type AsyncThunkConfig = {
+  state?: unknown
+  dispatch?: Dispatch
+  extra?: unknown
+  rejectValue?: unknown
+}
+
+type GetState<ThunkApiConfig> = ThunkApiConfig extends {
+  state: infer State
+}
+  ? State
+  : unknown
+type GetExtra<ThunkApiConfig> = ThunkApiConfig extends { extra: infer Extra }
+  ? Extra
+  : unknown
+type GetDispatch<ThunkApiConfig> = ThunkApiConfig extends {
+  dispatch: infer Dispatch
+}
+  ? FallbackIfUnknown<
+      Dispatch,
+      ThunkDispatch<
+        GetState<ThunkApiConfig>,
+        GetExtra<ThunkApiConfig>,
+        AnyAction
+      >
+    >
+  : ThunkDispatch<GetState<ThunkApiConfig>, GetExtra<ThunkApiConfig>, AnyAction>
+
+type GetThunkAPI<ThunkApiConfig> = BaseThunkAPI<
+  GetState<ThunkApiConfig>,
+  GetExtra<ThunkApiConfig>,
+  GetDispatch<ThunkApiConfig>,
+  GetRejectValue<ThunkApiConfig>
+>
+
+type GetRejectValue<ThunkApiConfig> = ThunkApiConfig extends {
+  rejectValue: infer RejectValue
+}
+  ? RejectValue
+  : unknown
+
+/**
+ *
+ * @param type
+ * @param payloadCreator
+ *
+ * @alpha
+ */
+export function createAsyncThunk<
+  Returned,
+  ThunkArg = void,
+  ThunkApiConfig extends AsyncThunkConfig = {}
+>(
+  type: string,
+  payloadCreator: (
+    arg: ThunkArg,
+    thunkAPI: GetThunkAPI<ThunkApiConfig>
+  ) =>
+    | Promise<Returned | RejectWithValue<GetRejectValue<ThunkApiConfig>>>
+    | Returned
+    | RejectWithValue<GetRejectValue<ThunkApiConfig>>
+) {
+  type RejectedValue = GetRejectValue<ThunkApiConfig>
+
+  const fulfilled = createAction(
+    type + '/fulfilled',
+    (result: Returned, requestId: string, arg: ThunkArg) => {
+      return {
+        payload: result,
+        meta: { arg, requestId }
+      }
+    }
+  )
+
+  const pending = createAction(
+    type + '/pending',
+    (requestId: string, arg: ThunkArg) => {
+      return {
+        payload: undefined,
+        meta: { arg, requestId }
+      }
+    }
+  )
+
+  const rejected = createAction(
+    type + '/rejected',
+    (
+      error: Error | null,
+      requestId: string,
+      arg: ThunkArg,
+      payload?: RejectedValue
+    ) => {
+      const aborted = !!error && error.name === 'AbortError'
+      return {
+        payload,
+        error: miniSerializeError(error || 'Rejected'),
+        meta: {
+          arg,
+          requestId,
+          aborted
+        }
+      }
+    }
+  )
+
+  let displayedWarning = false
+
+  const AC =
+    typeof AbortController !== 'undefined'
+      ? AbortController
+      : class implements AbortController {
+          signal: AbortSignal = {
+            aborted: false,
+            addEventListener() {},
+            dispatchEvent() {
+              return false
+            },
+            onabort() {},
+            removeEventListener() {}
+          }
+          abort() {
+            if (process.env.NODE_ENV !== 'production') {
+              if (!displayedWarning) {
+                displayedWarning = true
+                console.info(
+                  `This platform does not implement AbortController. 
+If you want to use the AbortController to react to \`abort\` events, please consider importing a polyfill like 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'.`
+                )
+              }
+            }
+          }
+        }
+
+  function actionCreator(arg: ThunkArg) {
+    return (
+      dispatch: GetDispatch<ThunkApiConfig>,
+      getState: () => GetState<ThunkApiConfig>,
+      extra: GetExtra<ThunkApiConfig>
+    ) => {
+      const requestId = nanoid()
+
+      const abortController = new AC()
+      let abortReason: string | undefined
+
+      const abortedPromise = new Promise<never>((_, reject) =>
+        abortController.signal.addEventListener('abort', () =>
+          reject({ name: 'AbortError', message: abortReason || 'Aborted' })
+        )
+      )
+
+      function abort(reason?: string) {
+        abortReason = reason
+        abortController.abort()
+      }
+
+      const promise = (async function() {
+        let finalAction: ReturnType<typeof fulfilled | typeof rejected>
+        try {
+          dispatch(pending(requestId, arg))
+          finalAction = await Promise.race([
+            abortedPromise,
+            Promise.resolve(
+              payloadCreator(arg, {
+                dispatch,
+                getState,
+                extra,
+                requestId,
+                signal: abortController.signal,
+                rejectWithValue(value: RejectedValue) {
+                  return new RejectWithValue(value)
+                }
+              })
+            ).then(result => {
+              if (result instanceof RejectWithValue) {
+                return rejected(null, requestId, arg, result.value)
+              }
+              return fulfilled(result, requestId, arg)
+            })
+          ])
+        } catch (err) {
+          finalAction = rejected(err, requestId, arg)
+        }
+        // We dispatch the result action _after_ the catch, to avoid having any errors
+        // here get swallowed by the try/catch block,
+        // per https://twitter.com/dan_abramov/status/770914221638942720
+        // and https://redux-toolkit.js.org/tutorials/advanced-tutorial#async-error-handling-logic-in-thunks
+
+        dispatch(finalAction)
+        return finalAction
+      })()
+      return Object.assign(promise, { abort })
+    }
+  }
+
+  return Object.assign(actionCreator, {
+    pending,
+    rejected,
+    fulfilled
+  })
+}
+
+type ActionTypesWithOptionalErrorAction =
+  | { error: any }
+  | { error?: never; payload: any }
+type PayloadForActionTypesExcludingErrorActions<T> = T extends { error: any }
+  ? never
+  : T extends { payload: infer P }
+  ? P
+  : never
+
+/**
+ * @alpha
+ */
+export function unwrapResult<R extends ActionTypesWithOptionalErrorAction>(
+  returned: R
+): PayloadForActionTypesExcludingErrorActions<R> {
+  if ('error' in returned) {
+    throw returned.error
+  }
+  return (returned as any).payload
+}
diff --git a/src/devtoolsExtension.ts b/src/devtoolsExtension.ts
new file mode 100644
index 00000000..88445aa2
--- /dev/null
+++ b/src/devtoolsExtension.ts
@@ -0,0 +1,201 @@
+import { Action, ActionCreator, StoreEnhancer, compose } from 'redux'
+
+/**
+ * @public
+ */
+export interface EnhancerOptions {
+  /**
+   * the instance name to be showed on the monitor page. Default value is `document.title`.
+   * If not specified and there's no document title, it will consist of `tabId` and `instanceId`.
+   */
+  name?: string
+  /**
+   * action creators functions to be available in the Dispatcher.
+   */
+  actionCreators?: ActionCreator<any>[] | { [key: string]: ActionCreator<any> }
+  /**
+   * if more than one action is dispatched in the indicated interval, all new actions will be collected and sent at once.
+   * It is the joint between performance and speed. When set to `0`, all actions will be sent instantly.
+   * Set it to a higher value when experiencing perf issues (also `maxAge` to a lower value).
+   *
+   * @default 500 ms.
+   */
+  latency?: number
+  /**
+   * (> 1) - maximum allowed actions to be stored in the history tree. The oldest actions are removed once maxAge is reached. It's critical for performance.
+   *
+   * @default 50
+   */
+  maxAge?: number
+  /**
+   * - `undefined` - will use regular `JSON.stringify` to send data (it's the fast mode).
+   * - `false` - will handle also circular references.
+   * - `true` - will handle also date, regex, undefined, error objects, symbols, maps, sets and functions.
+   * - object, which contains `date`, `regex`, `undefined`, `error`, `symbol`, `map`, `set` and `function` keys.
+   *   For each of them you can indicate if to include (by setting as `true`).
+   *   For `function` key you can also specify a custom function which handles serialization.
+   *   See [`jsan`](https://github.com/kolodny/jsan) for more details.
+   */
+  serialize?:
+    | boolean
+    | {
+        date?: boolean
+        regex?: boolean
+        undefined?: boolean
+        error?: boolean
+        symbol?: boolean
+        map?: boolean
+        set?: boolean
+        function?: boolean | Function
+      }
+  /**
+   * function which takes `action` object and id number as arguments, and should return `action` object back.
+   */
+  actionSanitizer?: <A extends Action>(action: A, id: number) => A
+  /**
+   * function which takes `state` object and index as arguments, and should return `state` object back.
+   */
+  stateSanitizer?: <S>(state: S, index: number) => S
+  /**
+   * *string or array of strings as regex* - actions types to be hidden / shown in the monitors (while passed to the reducers).
+   * If `actionsWhitelist` specified, `actionsBlacklist` is ignored.
+   */
+  actionsBlacklist?: string | string[]
+  /**
+   * *string or array of strings as regex* - actions types to be hidden / shown in the monitors (while passed to the reducers).
+   * If `actionsWhitelist` specified, `actionsBlacklist` is ignored.
+   */
+  actionsWhitelist?: string | string[]
+  /**
+   * called for every action before sending, takes `state` and `action` object, and returns `true` in case it allows sending the current data to the monitor.
+   * Use it as a more advanced version of `actionsBlacklist`/`actionsWhitelist` parameters.
+   */
+  predicate?: <S, A extends Action>(state: S, action: A) => boolean
+  /**
+   * if specified as `false`, it will not record the changes till clicking on `Start recording` button.
+   * Available only for Redux enhancer, for others use `autoPause`.
+   *
+   * @default true
+   */
+  shouldRecordChanges?: boolean
+  /**
+   * if specified, whenever clicking on `Pause recording` button and there are actions in the history log, will add this action type.
+   * If not specified, will commit when paused. Available only for Redux enhancer.
+   *
+   * @default "@@PAUSED""
+   */
+  pauseActionType?: string
+  /**
+   * auto pauses when the extension’s window is not opened, and so has zero impact on your app when not in use.
+   * Not available for Redux enhancer (as it already does it but storing the data to be sent).
+   *
+   * @default false
+   */
+  autoPause?: boolean
+  /**
+   * if specified as `true`, it will not allow any non-monitor actions to be dispatched till clicking on `Unlock changes` button.
+   * Available only for Redux enhancer.
+   *
+   * @default false
+   */
+  shouldStartLocked?: boolean
+  /**
+   * if set to `false`, will not recompute the states on hot reloading (or on replacing the reducers). Available only for Redux enhancer.
+   *
+   * @default true
+   */
+  shouldHotReload?: boolean
+  /**
+   * if specified as `true`, whenever there's an exception in reducers, the monitors will show the error message, and next actions will not be dispatched.
+   *
+   * @default false
+   */
+  shouldCatchErrors?: boolean
+  /**
+   * If you want to restrict the extension, specify the features you allow.
+   * If not specified, all of the features are enabled. When set as an object, only those included as `true` will be allowed.
+   * Note that except `true`/`false`, `import` and `export` can be set as `custom` (which is by default for Redux enhancer), meaning that the importing/exporting occurs on the client side.
+   * Otherwise, you'll get/set the data right from the monitor part.
+   */
+  features?: {
+    /**
+     * start/pause recording of dispatched actions
+     */
+    pause?: boolean
+    /**
+     * lock/unlock dispatching actions and side effects
+     */
+    lock?: boolean
+    /**
+     * persist states on page reloading
+     */
+    persist?: boolean
+    /**
+     * export history of actions in a file
+     */
+    export?: boolean | 'custom'
+    /**
+     * import history of actions from a file
+     */
+    import?: boolean | 'custom'
+    /**
+     * jump back and forth (time travelling)
+     */
+    jump?: boolean
+    /**
+     * skip (cancel) actions
+     */
+    skip?: boolean
+    /**
+     * drag and drop actions in the history list
+     */
+    reorder?: boolean
+    /**
+     * dispatch custom actions or action creators
+     */
+    dispatch?: boolean
+    /**
+     * generate tests for the selected actions
+     */
+    test?: boolean
+  }
+  /**
+   * Set to true or a stacktrace-returning function to record call stack traces for dispatched actions.
+   * Defaults to false.
+   */
+  trace?: boolean | (<A extends Action>(action: A) => string)
+  /**
+   * The maximum number of stack trace entries to record per action. Defaults to 10.
+   */
+  traceLimit?: number
+}
+
+/**
+ * @public
+ */
+export const composeWithDevTools: {
+  (options: EnhancerOptions): typeof compose
+  <StoreExt>(...funcs: Array<StoreEnhancer<StoreExt>>): StoreEnhancer<StoreExt>
+} =
+  typeof window !== 'undefined' &&
+  (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
+    ? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
+    : function() {
+        if (arguments.length === 0) return undefined
+        if (typeof arguments[0] === 'object') return compose
+        return compose.apply(null, (arguments as any) as Function[])
+      }
+
+/**
+ * @public
+ */
+export const devToolsEnhancer: {
+  (options: EnhancerOptions): StoreEnhancer<any>
+} =
+  typeof window !== 'undefined' && (window as any).__REDUX_DEVTOOLS_EXTENSION__
+    ? (window as any).__REDUX_DEVTOOLS_EXTENSION__
+    : function() {
+        return function(noop) {
+          return noop
+        }
+      }
diff --git a/src/entities/create_adapter.ts b/src/entities/create_adapter.ts
new file mode 100644
index 00000000..3325bcb4
--- /dev/null
+++ b/src/entities/create_adapter.ts
@@ -0,0 +1,38 @@
+import { EntityDefinition, Comparer, IdSelector, EntityAdapter } from './models'
+import { createInitialStateFactory } from './entity_state'
+import { createSelectorsFactory } from './state_selectors'
+import { createSortedStateAdapter } from './sorted_state_adapter'
+import { createUnsortedStateAdapter } from './unsorted_state_adapter'
+
+/**
+ *
+ * @param options
+ *
+ * @alpha
+ */
+export function createEntityAdapter<T>(
+  options: {
+    selectId?: IdSelector<T>
+    sortComparer?: false | Comparer<T>
+  } = {}
+): EntityAdapter<T> {
+  const { selectId, sortComparer }: EntityDefinition<T> = {
+    sortComparer: false,
+    selectId: (instance: any) => instance.id,
+    ...options
+  }
+
+  const stateFactory = createInitialStateFactory<T>()
+  const selectorsFactory = createSelectorsFactory<T>()
+  const stateAdapter = sortComparer
+    ? createSortedStateAdapter(selectId, sortComparer)
+    : createUnsortedStateAdapter(selectId)
+
+  return {
+    selectId,
+    sortComparer,
+    ...stateFactory,
+    ...selectorsFactory,
+    ...stateAdapter
+  }
+}
diff --git a/src/entities/entity_state.ts b/src/entities/entity_state.ts
new file mode 100644
index 00000000..a1eb977e
--- /dev/null
+++ b/src/entities/entity_state.ts
@@ -0,0 +1,20 @@
+import { EntityState } from './models'
+
+export function getInitialEntityState<V>(): EntityState<V> {
+  return {
+    ids: [],
+    entities: {}
+  }
+}
+
+export function createInitialStateFactory<V>() {
+  function getInitialState(): EntityState<V>
+  function getInitialState<S extends object>(
+    additionalState: S
+  ): EntityState<V> & S
+  function getInitialState(additionalState: any = {}): any {
+    return Object.assign(getInitialEntityState(), additionalState)
+  }
+
+  return { getInitialState }
+}
diff --git a/src/entities/fixtures/book.ts b/src/entities/fixtures/book.ts
new file mode 100644
index 00000000..6191374a
--- /dev/null
+++ b/src/entities/fixtures/book.ts
@@ -0,0 +1,19 @@
+export interface BookModel {
+  id: string
+  title: string
+}
+
+export const AClockworkOrange: BookModel = Object.freeze({
+  id: 'aco',
+  title: 'A Clockwork Orange'
+})
+
+export const AnimalFarm: BookModel = Object.freeze({
+  id: 'af',
+  title: 'Animal Farm'
+})
+
+export const TheGreatGatsby: BookModel = Object.freeze({
+  id: 'tgg',
+  title: 'The Great Gatsby'
+})
diff --git a/src/entities/index.ts b/src/entities/index.ts
new file mode 100644
index 00000000..e6fe1d87
--- /dev/null
+++ b/src/entities/index.ts
@@ -0,0 +1,9 @@
+export { createEntityAdapter } from './create_adapter'
+export {
+  Dictionary,
+  EntityState,
+  EntityAdapter,
+  Update,
+  IdSelector,
+  Comparer
+} from './models'
diff --git a/src/entities/models.ts b/src/entities/models.ts
new file mode 100644
index 00000000..4a569617
--- /dev/null
+++ b/src/entities/models.ts
@@ -0,0 +1,143 @@
+import { PayloadAction } from '../createAction'
+import { IsAny } from '../tsHelpers'
+
+/**
+ * @alpha
+ */
+export type EntityId = number | string
+
+/**
+ * @alpha
+ */
+export type Comparer<T> = (a: T, b: T) => number
+
+/**
+ * @alpha
+ */
+export type IdSelector<T> = (model: T) => EntityId
+
+/**
+ * @alpha
+ */
+export interface DictionaryNum<T> {
+  [id: number]: T | undefined
+}
+
+/**
+ * @alpha
+ */
+export interface Dictionary<T> extends DictionaryNum<T> {
+  [id: string]: T | undefined
+}
+
+/**
+ * @alpha
+ */
+export type Update<T> = { id: EntityId; changes: Partial<T> }
+
+/**
+ * @alpha
+ */
+export interface EntityState<T> {
+  ids: EntityId[]
+  entities: Dictionary<T>
+}
+
+export interface EntityDefinition<T> {
+  selectId: IdSelector<T>
+  sortComparer: false | Comparer<T>
+}
+
+export type PreventAny<S, T> = IsAny<S, EntityState<T>, S>
+
+export interface EntityStateAdapter<T> {
+  addOne<S extends EntityState<T>>(state: PreventAny<S, T>, entity: T): S
+  addOne<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    action: PayloadAction<T>
+  ): S
+
+  addMany<S extends EntityState<T>>(state: PreventAny<S, T>, entities: T[]): S
+  addMany<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    entities: PayloadAction<T[]>
+  ): S
+
+  setAll<S extends EntityState<T>>(state: PreventAny<S, T>, entities: T[]): S
+  setAll<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    entities: PayloadAction<T[]>
+  ): S
+
+  removeOne<S extends EntityState<T>>(state: PreventAny<S, T>, key: EntityId): S
+  removeOne<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    key: PayloadAction<EntityId>
+  ): S
+
+  removeMany<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    keys: EntityId[]
+  ): S
+  removeMany<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    keys: PayloadAction<EntityId[]>
+  ): S
+
+  removeAll<S extends EntityState<T>>(state: PreventAny<S, T>): S
+
+  updateOne<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    update: Update<T>
+  ): S
+  updateOne<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    update: PayloadAction<Update<T>>
+  ): S
+
+  updateMany<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    updates: Update<T>[]
+  ): S
+  updateMany<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    updates: PayloadAction<Update<T>[]>
+  ): S
+
+  upsertOne<S extends EntityState<T>>(state: PreventAny<S, T>, entity: T): S
+  upsertOne<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    entity: PayloadAction<T>
+  ): S
+
+  upsertMany<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    entities: T[]
+  ): S
+  upsertMany<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
+    entities: PayloadAction<T[]>
+  ): S
+}
+
+export interface EntitySelectors<T, V> {
+  selectIds: (state: V) => EntityId[]
+  selectEntities: (state: V) => Dictionary<T>
+  selectAll: (state: V) => T[]
+  selectTotal: (state: V) => number
+  selectById: (state: V, id: EntityId) => T | undefined
+}
+
+/**
+ * @alpha
+ */
+export interface EntityAdapter<T> extends EntityStateAdapter<T> {
+  selectId: IdSelector<T>
+  sortComparer: false | Comparer<T>
+  getInitialState(): EntityState<T>
+  getInitialState<S extends object>(state: S): EntityState<T> & S
+  getSelectors(): EntitySelectors<T, EntityState<T>>
+  getSelectors<V>(
+    selectState: (state: V) => EntityState<T>
+  ): EntitySelectors<T, V>
+}
diff --git a/src/entities/sorted_state_adapter.ts b/src/entities/sorted_state_adapter.ts
new file mode 100644
index 00000000..93c6e5e7
--- /dev/null
+++ b/src/entities/sorted_state_adapter.ts
@@ -0,0 +1,139 @@
+import {
+  EntityState,
+  IdSelector,
+  Comparer,
+  EntityStateAdapter,
+  Update
+} from './models'
+import { createStateOperator } from './state_adapter'
+import { createUnsortedStateAdapter } from './unsorted_state_adapter'
+import { selectIdValue } from './utils'
+
+export function createSortedStateAdapter<T>(
+  selectId: IdSelector<T>,
+  sort: Comparer<T>
+): EntityStateAdapter<T> {
+  type R = EntityState<T>
+
+  const { removeOne, removeMany, removeAll } = createUnsortedStateAdapter(
+    selectId
+  )
+
+  function addOneMutably(entity: T, state: R): void {
+    return addManyMutably([entity], state)
+  }
+
+  function addManyMutably(newModels: T[], state: R): void {
+    const models = newModels.filter(
+      model => !(selectIdValue(model, selectId) in state.entities)
+    )
+
+    if (models.length !== 0) {
+      merge(models, state)
+    }
+  }
+
+  function setAllMutably(models: T[], state: R): void {
+    state.entities = {}
+    state.ids = []
+
+    addManyMutably(models, state)
+  }
+
+  function updateOneMutably(update: Update<T>, state: R): void {
+    return updateManyMutably([update], state)
+  }
+
+  function takeUpdatedModel(models: T[], update: Update<T>, state: R): boolean {
+    if (!(update.id in state.entities)) {
+      return false
+    }
+
+    const original = state.entities[update.id]
+    const updated = Object.assign({}, original, update.changes)
+    const newKey = selectIdValue(updated, selectId)
+
+    delete state.entities[update.id]
+
+    models.push(updated)
+
+    return newKey !== update.id
+  }
+
+  function updateManyMutably(updates: Update<T>[], state: R): void {
+    const models: T[] = []
+
+    updates.forEach(update => takeUpdatedModel(models, update, state))
+
+    if (models.length !== 0) {
+      merge(models, state)
+    }
+  }
+
+  function upsertOneMutably(entity: T, state: R): void {
+    return upsertManyMutably([entity], state)
+  }
+
+  function upsertManyMutably(entities: T[], state: R): void {
+    const added: T[] = []
+    const updated: Update<T>[] = []
+
+    for (const entity of entities) {
+      const id = selectIdValue(entity, selectId)
+      if (id in state.entities) {
+        updated.push({ id, changes: entity })
+      } else {
+        added.push(entity)
+      }
+    }
+
+    updateManyMutably(updated, state)
+    addManyMutably(added, state)
+  }
+
+  function areArraysEqual(a: unknown[], b: unknown[]) {
+    if (a.length !== b.length) {
+      return false
+    }
+
+    for (let i = 0; i < a.length && i < b.length; i++) {
+      if (a[i] === b[i]) {
+        continue
+      }
+      return false
+    }
+    return true
+  }
+
+  function merge(models: T[], state: R): void {
+    models.sort(sort)
+
+    // Insert/overwrite all new/updated
+    models.forEach(model => {
+      state.entities[selectId(model)] = model
+    })
+
+    const allEntities = Object.values(state.entities) as T[]
+    allEntities.sort(sort)
+
+    const newSortedIds = allEntities.map(selectId)
+    const { ids } = state
+
+    if (!areArraysEqual(ids, newSortedIds)) {
+      state.ids = newSortedIds
+    }
+  }
+
+  return {
+    removeOne,
+    removeMany,
+    removeAll,
+    addOne: createStateOperator(addOneMutably),
+    updateOne: createStateOperator(updateOneMutably),
+    upsertOne: createStateOperator(upsertOneMutably),
+    setAll: createStateOperator(setAllMutably),
+    addMany: createStateOperator(addManyMutably),
+    updateMany: createStateOperator(updateManyMutably),
+    upsertMany: createStateOperator(upsertManyMutably)
+  }
+}
diff --git a/src/entities/state_adapter.ts b/src/entities/state_adapter.ts
new file mode 100644
index 00000000..a0008917
--- /dev/null
+++ b/src/entities/state_adapter.ts
@@ -0,0 +1,41 @@
+import createNextState, { isDraft } from 'immer'
+import { EntityState } from './models'
+import { PayloadAction, isFSA } from '../createAction'
+
+export function createStateOperator<V, R>(
+  mutator: (arg: R, state: EntityState<V>) => void
+) {
+  return function operation<S extends EntityState<V>>(
+    state: S,
+    arg: R | PayloadAction<R>
+  ): S {
+    function isPayloadActionArgument(
+      arg: R | PayloadAction<R>
+    ): arg is PayloadAction<R> {
+      return isFSA(arg)
+    }
+
+    const runMutator = (draft: EntityState<V>) => {
+      if (isPayloadActionArgument(arg)) {
+        mutator(arg.payload, draft)
+      } else {
+        mutator(arg, draft)
+      }
+    }
+
+    if (isDraft(state)) {
+      // we must already be inside a `createNextState` call, likely because
+      // this is being wrapped in `createReducer` or `createSlice`.
+      // It's safe to just pass the draft to the mutator.
+      runMutator(state)
+
+      // since it's a draft, we'll just return it
+      return state
+    } else {
+      // @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
+      // than an Immutable<S>, and TypeScript cannot find out how to reconcile
+      // these two types.
+      return createNextState(state, runMutator)
+    }
+  }
+}
diff --git a/src/entities/state_selectors.ts b/src/entities/state_selectors.ts
new file mode 100644
index 00000000..d7370c87
--- /dev/null
+++ b/src/entities/state_selectors.ts
@@ -0,0 +1,51 @@
+import { createSelector } from 'reselect'
+import { EntityState, EntitySelectors, Dictionary, EntityId } from './models'
+
+export function createSelectorsFactory<T>() {
+  function getSelectors(): EntitySelectors<T, EntityState<T>>
+  function getSelectors<V>(
+    selectState: (state: V) => EntityState<T>
+  ): EntitySelectors<T, V>
+  function getSelectors(
+    selectState?: (state: any) => EntityState<T>
+  ): EntitySelectors<T, any> {
+    const selectIds = (state: any) => state.ids
+
+    const selectEntities = (state: EntityState<T>) => state.entities
+
+    const selectAll = createSelector(
+      selectIds,
+      selectEntities,
+      (ids: T[], entities: Dictionary<T>): any =>
+        ids.map((id: any) => (entities as any)[id])
+    )
+
+    const selectId = (_: any, id: EntityId) => id
+
+    const selectById = (entities: Dictionary<T>, id: EntityId) => entities[id]
+
+    const selectTotal = createSelector(selectIds, ids => ids.length)
+
+    if (!selectState) {
+      return {
+        selectIds,
+        selectEntities,
+        selectAll,
+        selectTotal,
+        selectById: createSelector(selectEntities, selectId, selectById)
+      }
+    }
+
+    const selectGlobalizedEntities = createSelector(selectState, selectEntities)
+
+    return {
+      selectIds: createSelector(selectState, selectIds),
+      selectEntities: selectGlobalizedEntities,
+      selectAll: createSelector(selectState, selectAll),
+      selectTotal: createSelector(selectState, selectTotal),
+      selectById: createSelector(selectGlobalizedEntities, selectId, selectById)
+    }
+  }
+
+  return { getSelectors }
+}
diff --git a/src/entities/unsorted_state_adapter.ts b/src/entities/unsorted_state_adapter.ts
new file mode 100644
index 00000000..7386d9a8
--- /dev/null
+++ b/src/entities/unsorted_state_adapter.ts
@@ -0,0 +1,155 @@
+import {
+  EntityState,
+  EntityStateAdapter,
+  IdSelector,
+  Update,
+  EntityId
+} from './models'
+import { createStateOperator } from './state_adapter'
+import { selectIdValue } from './utils'
+
+export function createUnsortedStateAdapter<T>(
+  selectId: IdSelector<T>
+): EntityStateAdapter<T> {
+  type R = EntityState<T>
+
+  function addOneMutably(entity: T, state: EntityState<T>): void {
+    const key = selectIdValue(entity, selectId)
+
+    if (key in state.entities) {
+      return
+    }
+
+    state.ids.push(key)
+    state.entities[key] = entity
+  }
+
+  function addManyMutably(entities: T[], state: R): void {
+    for (const entity of entities) {
+      addOneMutably(entity, state)
+    }
+  }
+
+  function setAllMutably(entities: T[], state: R): void {
+    state.ids = []
+    state.entities = {}
+
+    addManyMutably(entities, state)
+  }
+
+  function removeOneMutably(key: EntityId, state: R): void {
+    return removeManyMutably([key], state)
+  }
+
+  function removeManyMutably(keys: EntityId[], state: R): void {
+    let didMutate = false
+
+    keys.forEach(key => {
+      if (key in state.entities) {
+        delete state.entities[key]
+        didMutate = true
+      }
+    })
+
+    if (didMutate) {
+      state.ids = state.ids.filter(id => id in state.entities)
+    }
+  }
+
+  function removeAll(state: R): any {
+    return Object.assign({}, state, {
+      ids: [],
+      entities: {}
+    })
+  }
+
+  function takeNewKey(
+    keys: { [id: string]: EntityId },
+    update: Update<T>,
+    state: R
+  ): boolean {
+    const original = state.entities[update.id]
+    const updated: T = Object.assign({}, original, update.changes)
+    const newKey = selectIdValue(updated, selectId)
+    const hasNewKey = newKey !== update.id
+
+    if (hasNewKey) {
+      keys[update.id] = newKey
+      delete state.entities[update.id]
+    }
+
+    state.entities[newKey] = updated
+
+    return hasNewKey
+  }
+
+  function updateOneMutably(update: Update<T>, state: R): void {
+    return updateManyMutably([update], state)
+  }
+
+  function updateManyMutably(updates: Update<T>[], state: R): void {
+    const newKeys: { [id: string]: EntityId } = {}
+
+    const updatesPerEntity: { [id: string]: Update<T> } = {}
+
+    updates.forEach(update => {
+      // Only apply updates to entities that currently exist
+      if (update.id in state.entities) {
+        // If there are multiple updates to one entity, merge them together
+        updatesPerEntity[update.id] = {
+          // Spreads ignore falsy values, so this works even if there isn't
+          // an existing update already at this key
+          ...updatesPerEntity[update.id],
+          ...update
+        }
+      }
+    })
+
+    updates = Object.values(updatesPerEntity)
+
+    const didMutateEntities = updates.length > 0
+
+    if (didMutateEntities) {
+      const didMutateIds =
+        updates.filter(update => takeNewKey(newKeys, update, state)).length > 0
+
+      if (didMutateIds) {
+        state.ids = state.ids.map(id => newKeys[id] || id)
+      }
+    }
+  }
+
+  function upsertOneMutably(entity: T, state: R): void {
+    return upsertManyMutably([entity], state)
+  }
+
+  function upsertManyMutably(entities: T[], state: R): void {
+    const added: T[] = []
+    const updated: Update<T>[] = []
+
+    for (const entity of entities) {
+      const id = selectIdValue(entity, selectId)
+      if (id in state.entities) {
+        updated.push({ id, changes: entity })
+      } else {
+        added.push(entity)
+      }
+    }
+
+    updateManyMutably(updated, state)
+    addManyMutably(added, state)
+  }
+
+  return {
+    removeAll,
+    addOne: createStateOperator(addOneMutably),
+    addMany: createStateOperator(addManyMutably),
+    setAll: createStateOperator(setAllMutably),
+    updateOne: createStateOperator(updateOneMutably),
+    updateMany: createStateOperator(updateManyMutably),
+    upsertOne: createStateOperator(upsertOneMutably),
+    upsertMany: createStateOperator(upsertManyMutably),
+    removeOne: createStateOperator(removeOneMutably),
+    removeMany: createStateOperator(removeManyMutably)
+  }
+}
diff --git a/src/entities/utils.spec.ts b/src/entities/utils.spec.ts
new file mode 100644
index 00000000..4dedcfa1
--- /dev/null
+++ b/src/entities/utils.spec.ts
@@ -0,0 +1,67 @@
+import { selectIdValue } from './utils'
+import { AClockworkOrange } from './fixtures/book'
+
+describe('Entity utils', () => {
+  describe(`selectIdValue()`, () => {
+    const OLD_ENV = process.env
+
+    beforeEach(() => {
+      jest.resetModules() // this is important - it clears the cache
+      process.env = { ...OLD_ENV, NODE_ENV: 'development' }
+    })
+
+    afterEach(() => {
+      process.env = OLD_ENV
+    })
+
+    it('should not warn when key does exist', () => {
+      const spy = spyOn(console, 'warn')
+
+      const key = selectIdValue(AClockworkOrange, book => book.id)
+
+      expect(spy).not.toHaveBeenCalled()
+    })
+
+    it('should warn when key does not exist in dev mode', () => {
+      const spy = spyOn(console, 'warn')
+
+      const key = selectIdValue(AClockworkOrange, (book: any) => book.foo)
+
+      expect(spy).toHaveBeenCalled()
+    })
+
+    it('should warn when key is undefined in dev mode', () => {
+      const spy = spyOn(console, 'warn')
+
+      const undefinedAClockworkOrange = { ...AClockworkOrange, id: undefined }
+      const key = selectIdValue(
+        undefinedAClockworkOrange,
+        (book: any) => book.id
+      )
+
+      expect(spy).toHaveBeenCalled()
+    })
+
+    it('should not warn when key does not exist in prod mode', () => {
+      process.env.NODE_ENV = 'production'
+      const spy = spyOn(console, 'warn')
+
+      const key = selectIdValue(AClockworkOrange, (book: any) => book.foo)
+
+      expect(spy).not.toHaveBeenCalled()
+    })
+
+    it('should not warn when key is undefined in prod mode', () => {
+      process.env.NODE_ENV = 'production'
+      const spy = spyOn(console, 'warn')
+
+      const undefinedAClockworkOrange = { ...AClockworkOrange, id: undefined }
+      const key = selectIdValue(
+        undefinedAClockworkOrange,
+        (book: any) => book.id
+      )
+
+      expect(spy).not.toHaveBeenCalled()
+    })
+  })
+})
diff --git a/src/entities/utils.ts b/src/entities/utils.ts
new file mode 100644
index 00000000..426c3bf0
--- /dev/null
+++ b/src/entities/utils.ts
@@ -0,0 +1,18 @@
+import { IdSelector } from './models'
+
+export function selectIdValue<T>(entity: T, selectId: IdSelector<T>) {
+  const key = selectId(entity)
+
+  if (process.env.NODE_ENV !== 'production' && key === undefined) {
+    console.warn(
+      'The entity passed to the `selectId` implementation returned undefined.',
+      'You should probably provide your own `selectId` implementation.',
+      'The entity that was passed:',
+      entity,
+      'The `selectId` implementation:',
+      selectId.toString()
+    )
+  }
+
+  return key
+}
diff --git a/src/getDefaultMiddleware.ts b/src/getDefaultMiddleware.ts
index c2135063..5efcf717 100644
--- a/src/getDefaultMiddleware.ts
+++ b/src/getDefaultMiddleware.ts
@@ -1,8 +1,11 @@
 import { Middleware, AnyAction } from 'redux'
 import thunkMiddleware, { ThunkMiddleware } from 'redux-thunk'
-/* PROD_START_REMOVE_UMD */
-import createImmutableStateInvariantMiddleware from 'redux-immutable-state-invariant'
-/* PROD_STOP_REMOVE_UMD */
+import {
+  /* PROD_START_REMOVE_UMD */
+  createImmutableStateInvariantMiddleware,
+  /* PROD_STOP_REMOVE_UMD */
+  ImmutableStateInvariantMiddlewareOptions
+} from './immutableStateInvariantMiddleware'
 
 import {
   createSerializableStateInvariantMiddleware,
@@ -17,11 +20,6 @@ interface ThunkOptions<E = any> {
   extraArgument: E
 }
 
-interface ImmutableStateInvariantMiddlewareOptions {
-  isImmutable?: (value: any) => boolean
-  ignore?: string[]
-}
-
 interface GetDefaultMiddlewareOptions {
   thunk?: boolean | ThunkOptions
   immutableCheck?: boolean | ImmutableStateInvariantMiddlewareOptions
diff --git a/src/immutableStateInvariantMiddleware.ts b/src/immutableStateInvariantMiddleware.ts
new file mode 100644
index 00000000..b80f3731
--- /dev/null
+++ b/src/immutableStateInvariantMiddleware.ts
@@ -0,0 +1,250 @@
+import { Middleware } from 'redux'
+import { getTimeMeasureUtils } from './utils'
+
+type EntryProcessor = (key: string, value: any) => any
+
+const isProduction: boolean = process.env.NODE_ENV === 'production'
+const prefix: string = 'Invariant failed'
+
+// Throw an error if the condition fails
+// Strip out error messages for production
+// > Not providing an inline default argument for message as the result is smaller
+function invariant(condition: any, message?: string) {
+  if (condition) {
+    return
+  }
+  // Condition not passed
+
+  // In production we strip the message but still throw
+  if (isProduction) {
+    throw new Error(prefix)
+  }
+
+  // When not in production we allow the message to pass through
+  // *This block will be removed in production builds*
+  throw new Error(`${prefix}: ${message || ''}`)
+}
+
+function stringify(
+  obj: any,
+  serializer?: EntryProcessor,
+  indent?: string | number,
+  decycler?: EntryProcessor
+): string {
+  return JSON.stringify(obj, getSerialize(serializer, decycler), indent)
+}
+
+function getSerialize(
+  serializer?: EntryProcessor,
+  decycler?: EntryProcessor
+): EntryProcessor {
+  let stack: any[] = [],
+    keys: any[] = []
+
+  if (!decycler)
+    decycler = function(_: string, value: any) {
+      if (stack[0] === value) return '[Circular ~]'
+      return (
+        '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']'
+      )
+    }
+
+  return function(this: any, key: string, value: any) {
+    if (stack.length > 0) {
+      var thisPos = stack.indexOf(this)
+      ~thisPos ? stack.splice(thisPos + 1) : stack.push(this)
+      ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key)
+      if (~stack.indexOf(value)) value = decycler!.call(this, key, value)
+    } else stack.push(value)
+
+    return serializer == null ? value : serializer.call(this, key, value)
+  }
+}
+
+export function isImmutableDefault(value: unknown): boolean {
+  return (
+    typeof value !== 'object' || value === null || typeof value === 'undefined'
+  )
+}
+
+export function trackForMutations(
+  isImmutable: IsImmutableFunc,
+  ingorePaths: string[] | undefined,
+  obj: any
+) {
+  const trackedProperties = trackProperties(isImmutable, ingorePaths, obj)
+  return {
+    detectMutations() {
+      return detectMutations(isImmutable, ingorePaths, trackedProperties, obj)
+    }
+  }
+}
+
+interface TrackedProperty {
+  value: any
+  children: Record<string, any>
+}
+
+function trackProperties(
+  isImmutable: IsImmutableFunc,
+  ignorePaths: IgnorePaths = [],
+  obj: Record<string, any>,
+  path: string[] = []
+) {
+  const tracked: Partial<TrackedProperty> = { value: obj }
+
+  if (!isImmutable(obj)) {
+    tracked.children = {}
+
+    for (const key in obj) {
+      const childPath = path.concat(key)
+      if (
+        ignorePaths.length &&
+        ignorePaths.indexOf(childPath.join('.')) !== -1
+      ) {
+        continue
+      }
+
+      tracked.children[key] = trackProperties(
+        isImmutable,
+        ignorePaths,
+        obj[key],
+        childPath
+      )
+    }
+  }
+  return tracked as TrackedProperty
+}
+
+type IgnorePaths = string[]
+
+function detectMutations(
+  isImmutable: IsImmutableFunc,
+  ignorePaths: IgnorePaths = [],
+  trackedProperty: TrackedProperty,
+  obj: any,
+  sameParentRef: boolean = false,
+  path: string[] = []
+): { wasMutated: boolean; path?: string[] } {
+  const prevObj = trackedProperty ? trackedProperty.value : undefined
+
+  const sameRef = prevObj === obj
+
+  if (sameParentRef && !sameRef && !Number.isNaN(obj)) {
+    return { wasMutated: true, path }
+  }
+
+  if (isImmutable(prevObj) || isImmutable(obj)) {
+    return { wasMutated: false }
+  }
+
+  // Gather all keys from prev (tracked) and after objs
+  const keysToDetect: Record<string, boolean> = {}
+  Object.keys(trackedProperty.children).forEach(key => {
+    keysToDetect[key] = true
+  })
+  Object.keys(obj).forEach(key => {
+    keysToDetect[key] = true
+  })
+
+  const keys = Object.keys(keysToDetect)
+  for (let i = 0; i < keys.length; i++) {
+    const key = keys[i]
+    const childPath = path.concat(key)
+    if (ignorePaths.length && ignorePaths.indexOf(childPath.join('.')) !== -1) {
+      continue
+    }
+
+    const result = detectMutations(
+      isImmutable,
+      ignorePaths,
+      trackedProperty.children[key],
+      obj[key],
+      sameRef,
+      childPath
+    )
+
+    if (result.wasMutated) {
+      return result
+    }
+  }
+  return { wasMutated: false }
+}
+
+type IsImmutableFunc = (value: any) => boolean
+export interface ImmutableStateInvariantMiddlewareOptions {
+  isImmutable?: IsImmutableFunc
+  ignoredPaths?: string[]
+  warnAfter?: number
+}
+
+export function createImmutableStateInvariantMiddleware(
+  options: ImmutableStateInvariantMiddlewareOptions = {}
+): Middleware {
+  if (process.env.NODE_ENV === 'production') {
+    return () => next => action => next(action)
+  }
+
+  const {
+    isImmutable = isImmutableDefault,
+    ignoredPaths,
+    warnAfter = 32
+  } = options
+  const track = trackForMutations.bind(null, isImmutable, ignoredPaths)
+
+  return ({ getState }) => {
+    let state = getState()
+    let tracker = track(state)
+
+    let result
+    return next => action => {
+      const measureUtils = getTimeMeasureUtils(
+        warnAfter,
+        'ImmutableStateInvariantMiddleware'
+      )
+
+      measureUtils.measureTime(() => {
+        state = getState()
+
+        result = tracker.detectMutations()
+        // Track before potentially not meeting the invariant
+        tracker = track(state)
+
+        invariant(
+          !result.wasMutated,
+          `A state mutation was detected between dispatches, in the path '${(
+            result.path || []
+          ).join(
+            '.'
+          )}'.  This may cause incorrect behavior. (http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments)`
+        )
+      })
+
+      const dispatchedAction = next(action)
+
+      measureUtils.measureTime(() => {
+        state = getState()
+
+        result = tracker.detectMutations()
+        // Track before potentially not meeting the invariant
+        tracker = track(state)
+
+        result.wasMutated &&
+          invariant(
+            !result.wasMutated,
+            `A state mutation was detected inside a dispatch, in the path: ${(
+              result.path || []
+            ).join(
+              '.'
+            )}. Take a look at the reducer(s) handling the action ${stringify(
+              action
+            )}. (http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments)`
+          )
+      })
+
+      measureUtils.warnIfExceeded()
+
+      return dispatchedAction
+    }
+  }
+}
diff --git a/src/index.ts b/src/index.ts
index 1f22243e..670d542c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,8 +1,21 @@
+import { enableES5 } from 'immer'
 export * from 'redux'
 export { default as createNextState, Draft } from 'immer'
-export { createSelector } from 'reselect'
+export {
+  createSelector,
+  Selector,
+  OutputParametricSelector,
+  OutputSelector,
+  ParametricSelector
+} from 'reselect'
 export { ThunkAction } from 'redux-thunk'
 
+// We deliberately enable Immer's ES5 support, on the grounds that
+// we assume RTK will be used with React Native and other Proxy-less
+// environments.  In addition, that's how Immer 4 behaved, and since
+// we want to ship this in an RTK minor, we should keep the same behavior.
+enableES5()
+
 export {
   // js
   configureStore,
@@ -61,3 +74,19 @@ export {
   // types
   ActionReducerMapBuilder
 } from './mapBuilders'
+
+export { createEntityAdapter } from './entities/create_adapter'
+export {
+  Dictionary,
+  EntityState,
+  EntityAdapter,
+  Update,
+  IdSelector,
+  Comparer
+} from './entities/models'
+
+export {
+  createAsyncThunk,
+  unwrapResult,
+  SerializedError
+} from './createAsyncThunk'
diff --git a/src/nanoid.ts b/src/nanoid.ts
new file mode 100644
index 00000000..a025110a
--- /dev/null
+++ b/src/nanoid.ts
@@ -0,0 +1,27 @@
+// Borrowed from https://github.com/ai/nanoid/tree/master/non-secure
+// This alphabet uses a-z A-Z 0-9 _- symbols.
+// Symbols are generated for smaller size.
+// -_zyxwvutsrqponmlkjihgfedcba9876543210ZYXWVUTSRQPONMLKJIHGFEDCBA
+let url = '-_'
+// Loop from 36 to 0 (from z to a and 9 to 0 in Base36).
+let i = 36
+while (i--) {
+  // 36 is radix. Number.prototype.toString(36) returns number
+  // in Base36 representation. Base36 is like hex, but it uses 0–9 and a-z.
+  url += i.toString(36)
+}
+// Loop from 36 to 10 (from Z to A in Base36).
+i = 36
+while (i-- - 10) {
+  url += i.toString(36).toUpperCase()
+}
+
+export function nanoid(size = 21) {
+  let id = ''
+  // Compact alternative for `for (var i = 0; i < size; i++)`
+  while (size--) {
+    // `| 0` is compact and faster alternative for `Math.floor()`
+    id += url[(Math.random() * 64) | 0]
+  }
+  return id
+}
diff --git a/src/serializableStateInvariantMiddleware.ts b/src/serializableStateInvariantMiddleware.ts
index 9b5f610a..a4955425 100644
--- a/src/serializableStateInvariantMiddleware.ts
+++ b/src/serializableStateInvariantMiddleware.ts
@@ -1,5 +1,6 @@
 import isPlainObject from './isPlainObject'
 import { Middleware } from 'redux'
+import { getTimeMeasureUtils } from './utils'
 
 /**
  * Returns true if the passed value is "plain", i.e. a value that is either
@@ -35,7 +36,7 @@ export function findNonSerializableValue(
   path: ReadonlyArray<string> = [],
   isSerializable: (value: unknown) => boolean = isPlain,
   getEntries?: (value: unknown) => [string, any][],
-  ignoredPaths: string[] = []
+  ignoredPaths: string[] = ['meta.args']
 ): NonSerializableValue | false {
   let foundNestedSerializable: NonSerializableValue | false
 
@@ -114,6 +115,10 @@ export interface SerializableStateInvariantMiddlewareOptions {
    * An array of dot-separated path strings to ignore when checking for serializability, Defaults to []
    */
   ignoredPaths?: string[]
+  /**
+   * Execution time warning threshold. If the middleware takes longer than `warnAfter` ms, a warning will be displayed in the console. Defaults to 32
+   */
+  warnAfter?: number
 }
 
 /**
@@ -128,11 +133,15 @@ export interface SerializableStateInvariantMiddlewareOptions {
 export function createSerializableStateInvariantMiddleware(
   options: SerializableStateInvariantMiddlewareOptions = {}
 ): Middleware {
+  if (process.env.NODE_ENV === 'production') {
+    return () => next => action => next(action)
+  }
   const {
     isSerializable = isPlain,
     getEntries,
     ignoredActions = [],
-    ignoredPaths = []
+    ignoredPaths = [],
+    warnAfter = 32
   } = options
 
   return storeAPI => next => action => {
@@ -140,48 +149,58 @@ export function createSerializableStateInvariantMiddleware(
       return next(action)
     }
 
-    const foundActionNonSerializableValue = findNonSerializableValue(
-      action,
-      [],
-      isSerializable,
-      getEntries
+    const measureUtils = getTimeMeasureUtils(
+      warnAfter,
+      'SerializableStateInvariantMiddleware'
     )
-
-    if (foundActionNonSerializableValue) {
-      const { keyPath, value } = foundActionNonSerializableValue
-
-      console.error(
-        `A non-serializable value was detected in an action, in the path: \`${keyPath}\`. Value:`,
-        value,
-        '\nTake a look at the logic that dispatched this action: ',
+    measureUtils.measureTime(() => {
+      const foundActionNonSerializableValue = findNonSerializableValue(
         action,
-        '\n(See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)'
+        [],
+        isSerializable,
+        getEntries
       )
-    }
+
+      if (foundActionNonSerializableValue) {
+        const { keyPath, value } = foundActionNonSerializableValue
+
+        console.error(
+          `A non-serializable value was detected in an action, in the path: \`${keyPath}\`. Value:`,
+          value,
+          '\nTake a look at the logic that dispatched this action: ',
+          action,
+          '\n(See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)'
+        )
+      }
+    })
 
     const result = next(action)
 
-    const state = storeAPI.getState()
+    measureUtils.measureTime(() => {
+      const state = storeAPI.getState()
 
-    const foundStateNonSerializableValue = findNonSerializableValue(
-      state,
-      [],
-      isSerializable,
-      getEntries,
-      ignoredPaths
-    )
+      const foundStateNonSerializableValue = findNonSerializableValue(
+        state,
+        [],
+        isSerializable,
+        getEntries,
+        ignoredPaths
+      )
 
-    if (foundStateNonSerializableValue) {
-      const { keyPath, value } = foundStateNonSerializableValue
+      if (foundStateNonSerializableValue) {
+        const { keyPath, value } = foundStateNonSerializableValue
 
-      console.error(
-        `A non-serializable value was detected in the state, in the path: \`${keyPath}\`. Value:`,
-        value,
-        `
+        console.error(
+          `A non-serializable value was detected in the state, in the path: \`${keyPath}\`. Value:`,
+          value,
+          `
 Take a look at the reducer(s) handling this action type: ${action.type}.
 (See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)`
-      )
-    }
+        )
+      }
+    })
+
+    measureUtils.warnIfExceeded()
 
     return result
   }
diff --git a/src/tsHelpers.ts b/src/tsHelpers.ts
index d5dcda74..d624cd35 100644
--- a/src/tsHelpers.ts
+++ b/src/tsHelpers.ts
@@ -20,6 +20,8 @@ export type IsUnknown<T, True, False = never> = unknown extends T
   ? IsAny<T, False, True>
   : False
 
+export type FallbackIfUnknown<T, Fallback> = IsUnknown<T, Fallback, T>
+
 /**
  * @internal
  */
@@ -83,8 +85,8 @@ export type DispatchForMiddlewares<M> = M extends ReadonlyArray<any>
  * Convert a Union type `(A|B)` to and intersecion type `(A&B)`
  */
 type UnionToIntersection<U> = (U extends any
-  ? (k: U) => void
-  : never) extends ((k: infer I) => void)
+? (k: U) => void
+: never) extends (k: infer I) => void
   ? I
   : never
 
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 00000000..933c7f4a
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,21 @@
+export function getTimeMeasureUtils(maxDelay: number, fnName: string) {
+  let elapsed = 0
+  return {
+    measureTime<T>(fn: () => T): T {
+      const started = Date.now()
+      try {
+        return fn()
+      } finally {
+        const finished = Date.now()
+        elapsed += finished - started
+      }
+    },
+    warnIfExceeded() {
+      if (elapsed > maxDelay) {
+        console.warn(`${fnName} took ${elapsed}ms, which is more than the warning threshold of ${maxDelay}ms. 
+If your state or actions are very large, you may want to disable the middleware as it might cause too much of a slowdown in development mode. See https://redux-toolkit.js.org/api/getDefaultMiddleware for instructions.
+It is disabled in production builds, so you don't need to worry about that.`)
+      }
+    }
+  }
+}
diff --git a/website/sidebars.json b/website/sidebars.json
index 5aa3b342..a7bbf212 100644
--- a/website/sidebars.json
+++ b/website/sidebars.json
@@ -14,6 +14,8 @@
       "api/createAction",
       "api/createSlice",
       "api/createSelector",
+      "api/createAsyncThunk",
+      "api/createEntityAdapter",
       "api/other-exports"
     ]
   }


Test patch:
diff --git a/src/combinedTest.test.ts b/src/combinedTest.test.ts
new file mode 100644
index 00000000..5217a583
--- /dev/null
+++ b/src/combinedTest.test.ts
@@ -0,0 +1,130 @@
+import { createAsyncThunk } from './createAsyncThunk'
+import { createAction, PayloadAction } from './createAction'
+import { createSlice } from './createSlice'
+import { configureStore } from './configureStore'
+import { createEntityAdapter } from './entities/create_adapter'
+import { EntityAdapter } from './entities/models'
+import { BookModel } from './entities/fixtures/book'
+
+describe('Combined entity slice', () => {
+  let adapter: EntityAdapter<BookModel>
+
+  beforeEach(() => {
+    adapter = createEntityAdapter({
+      selectId: (book: BookModel) => book.id,
+      sortComparer: (a, b) => a.title.localeCompare(b.title)
+    })
+  })
+
+  it('Entity and async features all works together', async () => {
+    const upsertBook = createAction<BookModel>('otherBooks/upsert')
+
+    type BooksState = ReturnType<typeof adapter.getInitialState> & {
+      loading: 'initial' | 'pending' | 'finished' | 'failed'
+      lastRequestId: string | null
+    }
+
+    const initialState: BooksState = adapter.getInitialState({
+      loading: 'initial',
+      lastRequestId: null
+    })
+
+    const fakeBooks: BookModel[] = [
+      { id: 'b', title: 'Second' },
+      { id: 'a', title: 'First' }
+    ]
+
+    const fetchBooksTAC = createAsyncThunk<
+      BookModel[],
+      void,
+      {
+        state: { books: BooksState }
+      }
+    >(
+      'books/fetch',
+      async (arg, { getState, dispatch, extra, requestId, signal }) => {
+        const state = getState()
+        return fakeBooks
+      }
+    )
+
+    const booksSlice = createSlice({
+      name: 'books',
+      initialState,
+      reducers: {
+        addOne: adapter.addOne,
+        removeOne(state, action: PayloadAction<string>) {
+          const sizeBefore = state.ids.length
+          // Originally, having nested `produce` calls don't mutate `state` here as I would have expected.
+          // (note that `state` here is actually an Immer Draft<S>, from `createReducer`)
+          // One woarkound was to return the new plain result value instead
+          // See https://github.com/immerjs/immer/issues/533
+          // However, after tweaking `createStateOperator` to check if the argument is a draft,
+          // we can just treat the operator as strictly mutating, without returning a result,
+          // and the result should be correct.
+          const result = adapter.removeOne(state, action)
+
+          const sizeAfter = state.ids.length
+          if (sizeBefore > 0) {
+            expect(sizeAfter).toBe(sizeBefore - 1)
+          }
+
+          //Deliberately _don't_ return result
+        }
+      },
+      extraReducers: builder => {
+        builder.addCase(upsertBook, (state, action) => {
+          return adapter.upsertOne(state, action)
+        })
+        builder.addCase(fetchBooksTAC.pending, (state, action) => {
+          state.loading = 'pending'
+          state.lastRequestId = action.meta.requestId
+        })
+        builder.addCase(fetchBooksTAC.fulfilled, (state, action) => {
+          if (
+            state.loading === 'pending' &&
+            action.meta.requestId === state.lastRequestId
+          ) {
+            adapter.setAll(state, action.payload)
+            state.loading = 'finished'
+            state.lastRequestId = null
+          }
+        })
+      }
+    })
+
+    const { addOne, removeOne } = booksSlice.actions
+    const { reducer } = booksSlice
+
+    const store = configureStore({
+      reducer: {
+        books: reducer
+      }
+    })
+
+    await store.dispatch(fetchBooksTAC())
+
+    const { books: booksAfterLoaded } = store.getState()
+    // Sorted, so "First" goes first
+    expect(booksAfterLoaded.ids).toEqual(['a', 'b'])
+    expect(booksAfterLoaded.lastRequestId).toBe(null)
+    expect(booksAfterLoaded.loading).toBe('finished')
+
+    store.dispatch(addOne({ id: 'd', title: 'Remove Me' }))
+    store.dispatch(removeOne('d'))
+
+    store.dispatch(addOne({ id: 'c', title: 'Middle' }))
+
+    const { books: booksAfterAddOne } = store.getState()
+
+    // Sorted, so "Middle" goes in the middle
+    expect(booksAfterAddOne.ids).toEqual(['a', 'c', 'b'])
+
+    store.dispatch(upsertBook({ id: 'c', title: 'Zeroth' }))
+
+    const { books: booksAfterUpsert } = store.getState()
+
+    // Sorted, so "Zeroth" goes last
+    expect(booksAfterUpsert.ids).toEqual(['a', 'b', 'c'])
+  })
+})
diff --git a/src/configureStore.test.ts b/src/configureStore.test.ts
index 4d6290c6..a7a4bab6 100644
--- a/src/configureStore.test.ts
+++ b/src/configureStore.test.ts
@@ -1,6 +1,6 @@
 import { configureStore } from './configureStore'
 import * as redux from 'redux'
-import * as devtools from 'redux-devtools-extension'
+import * as devtools from './devtoolsExtension'
 import { StoreEnhancer, StoreEnhancerStoreCreator } from 'redux'
 
 describe('configureStore', () => {
diff --git a/src/createAsyncThunk.test.ts b/src/createAsyncThunk.test.ts
new file mode 100644
index 00000000..c1a5c150
--- /dev/null
+++ b/src/createAsyncThunk.test.ts
@@ -0,0 +1,465 @@
+import {
+  createAsyncThunk,
+  miniSerializeError,
+  unwrapResult
+} from './createAsyncThunk'
+import { configureStore } from './configureStore'
+import { AnyAction } from 'redux'
+
+import {
+  mockConsole,
+  createConsole,
+  getLog
+} from 'console-testing-library/pure'
+
+declare global {
+  interface Window {
+    AbortController: AbortController
+  }
+}
+
+describe('createAsyncThunk', () => {
+  it('creates the action types', () => {
+    const thunkActionCreator = createAsyncThunk('testType', async () => 42)
+
+    expect(thunkActionCreator.fulfilled.type).toBe('testType/fulfilled')
+    expect(thunkActionCreator.pending.type).toBe('testType/pending')
+    expect(thunkActionCreator.rejected.type).toBe('testType/rejected')
+  })
+
+  it('works without passing arguments to the payload creator', async () => {
+    const thunkActionCreator = createAsyncThunk('testType', async () => 42)
+
+    let timesReducerCalled = 0
+
+    const reducer = () => {
+      timesReducerCalled++
+    }
+
+    const store = configureStore({
+      reducer
+    })
+
+    // reset from however many times the store called it
+    timesReducerCalled = 0
+
+    await store.dispatch(thunkActionCreator())
+
+    expect(timesReducerCalled).toBe(2)
+  })
+
+  it('accepts arguments and dispatches the actions on resolve', async () => {
+    const dispatch = jest.fn()
+
+    let passedArg: any
+
+    const result = 42
+    const args = 123
+    let generatedRequestId = ''
+
+    const thunkActionCreator = createAsyncThunk(
+      'testType',
+      async (arg: number, { requestId }) => {
+        passedArg = arg
+        generatedRequestId = requestId
+        return result
+      }
+    )
+
+    const thunkFunction = thunkActionCreator(args)
+
+    await thunkFunction(dispatch, () => {}, undefined)
+
+    expect(passedArg).toBe(args)
+
+    expect(dispatch).toHaveBeenNthCalledWith(
+      1,
+      thunkActionCreator.pending(generatedRequestId, args)
+    )
+
+    expect(dispatch).toHaveBeenNthCalledWith(
+      2,
+      thunkActionCreator.fulfilled(result, generatedRequestId, args)
+    )
+  })
+
+  it('accepts arguments and dispatches the actions on reject', async () => {
+    const dispatch = jest.fn()
+
+    const args = 123
+    let generatedRequestId = ''
+
+    const error = new Error('Panic!')
+
+    const thunkActionCreator = createAsyncThunk(
+      'testType',
+      async (args: number, { requestId }) => {
+        generatedRequestId = requestId
+        throw error
+      }
+    )
+
+    const thunkFunction = thunkActionCreator(args)
+
+    try {
+      await thunkFunction(dispatch, () => {}, undefined)
+    } catch (e) {}
+
+    expect(dispatch).toHaveBeenNthCalledWith(
+      1,
+      thunkActionCreator.pending(generatedRequestId, args)
+    )
+
+    expect(dispatch).toHaveBeenCalledTimes(2)
+
+    // Have to check the bits of the action separately since the error was processed
+    const errorAction = dispatch.mock.calls[1][0]
+    expect(errorAction.error).toEqual(miniSerializeError(error))
+    expect(errorAction.meta.requestId).toBe(generatedRequestId)
+    expect(errorAction.meta.arg).toBe(args)
+  })
+
+  it('dispatches an empty error when throwing a random object without serializedError properties', async () => {
+    const dispatch = jest.fn()
+
+    const args = 123
+    let generatedRequestId = ''
+
+    const errorObject = { wny: 'dothis' }
+
+    const thunkActionCreator = createAsyncThunk(
+      'testType',
+      async (args: number, { requestId }) => {
+        generatedRequestId = requestId
+        throw errorObject
+      }
+    )
+
+    const thunkFunction = thunkActionCreator(args)
+
+    try {
+      await thunkFunction(dispatch, () => {}, undefined)
+    } catch (e) {}
+
+    expect(dispatch).toHaveBeenNthCalledWith(
+      1,
+      thunkActionCreator.pending(generatedRequestId, args)
+    )
+
+    expect(dispatch).toHaveBeenCalledTimes(2)
+
+    const errorAction = dispatch.mock.calls[1][0]
+    expect(errorAction.error).toEqual({})
+    expect(errorAction.meta.requestId).toBe(generatedRequestId)
+    expect(errorAction.meta.arg).toBe(args)
+  })
+
+  it('dispatches an action with a formatted error when throwing an object with known error keys', async () => {
+    const dispatch = jest.fn()
+
+    const args = 123
+    let generatedRequestId = ''
+
+    const errorObject = {
+      name: 'Custom thrown error',
+      message: 'This is not necessary',
+      code: '400'
+    }
+
+    const thunkActionCreator = createAsyncThunk(
+      'testType',
+      async (args: number, { requestId }) => {
+        generatedRequestId = requestId
+        throw errorObject
+      }
+    )
+
+    const thunkFunction = thunkActionCreator(args)
+
+    try {
+      await thunkFunction(dispatch, () => {}, undefined)
+    } catch (e) {}
+
+    expect(dispatch).toHaveBeenNthCalledWith(
+      1,
+      thunkActionCreator.pending(generatedRequestId, args)
+    )
+
+    expect(dispatch).toHaveBeenCalledTimes(2)
+
+    // Have to check the bits of the action separately since the error was processed
+    const errorAction = dispatch.mock.calls[1][0]
+    expect(errorAction.error).toEqual(miniSerializeError(errorObject))
+    expect(Object.keys(errorAction.error)).not.toContain('stack')
+    expect(errorAction.meta.requestId).toBe(generatedRequestId)
+    expect(errorAction.meta.arg).toBe(args)
+  })
+
+  it('dispatches a rejected action with a customized payload when a user returns rejectWithValue()', async () => {
+    const dispatch = jest.fn()
+
+    const args = 123
+    let generatedRequestId = ''
+
+    const errorPayload = {
+      errorMessage:
+        'I am a fake server-provided 400 payload with validation details',
+      errors: [
+        { field_one: 'Must be a string' },
+        { field_two: 'Must be a number' }
+      ]
+    }
+
+    const thunkActionCreator = createAsyncThunk(
+      'testType',
+      async (args: number, { requestId, rejectWithValue }) => {
+        generatedRequestId = requestId
+
+        return rejectWithValue(errorPayload)
+      }
+    )
+
+    const thunkFunction = thunkActionCreator(args)
+
+    try {
+      await thunkFunction(dispatch, () => {}, undefined)
+    } catch (e) {}
+
+    expect(dispatch).toHaveBeenNthCalledWith(
+      1,
+      thunkActionCreator.pending(generatedRequestId, args)
+    )
+
+    expect(dispatch).toHaveBeenCalledTimes(2)
+
+    // Have to check the bits of the action separately since the error was processed
+    const errorAction = dispatch.mock.calls[1][0]
+
+    expect(errorAction.error.message).toEqual('Rejected')
+    expect(errorAction.payload).toBe(errorPayload)
+    expect(errorAction.meta.arg).toBe(args)
+  })
+
+  it('dispatches a rejected action with a miniSerializeError when rejectWithValue conditions are not satisfied', async () => {
+    const dispatch = jest.fn()
+
+    const args = 123
+    let generatedRequestId = ''
+
+    const error = new Error('Panic!')
+
+    const errorPayload = {
+      errorMessage:
+        'I am a fake server-provided 400 payload with validation details',
+      errors: [
+        { field_one: 'Must be a string' },
+        { field_two: 'Must be a number' }
+      ]
+    }
+
+    const thunkActionCreator = createAsyncThunk(
+      'testType',
+      async (args: number, { requestId, rejectWithValue }) => {
+        generatedRequestId = requestId
+
+        try {
+          throw error
+        } catch (err) {
+          if (!err.response) {
+            throw err
+          }
+          return rejectWithValue(errorPayload)
+        }
+      }
+    )
+
+    const thunkFunction = thunkActionCreator(args)
+
+    try {
+      await thunkFunction(dispatch, () => {}, undefined)
+    } catch (e) {}
+
+    expect(dispatch).toHaveBeenNthCalledWith(
+      1,
+      thunkActionCreator.pending(generatedRequestId, args)
+    )
+
+    expect(dispatch).toHaveBeenCalledTimes(2)
+
+    // Have to check the bits of the action separately since the error was processed
+    const errorAction = dispatch.mock.calls[1][0]
+    expect(errorAction.error).toEqual(miniSerializeError(error))
+    expect(errorAction.payload).toEqual(undefined)
+    expect(errorAction.meta.requestId).toBe(generatedRequestId)
+    expect(errorAction.meta.arg).toBe(args)
+  })
+})
+
+describe('createAsyncThunk with abortController', () => {
+  const asyncThunk = createAsyncThunk('test', function abortablePayloadCreator(
+    _: any,
+    { signal }
+  ) {
+    return new Promise((resolve, reject) => {
+      if (signal.aborted) {
+        reject(
+          new DOMException(
+            'This should never be reached as it should already be handled.',
+            'AbortError'
+          )
+        )
+      }
+      signal.addEventListener('abort', () => {
+        reject(new DOMException('Was aborted while running', 'AbortError'))
+      })
+      setTimeout(resolve, 100)
+    })
+  })
+
+  let store = configureStore({
+    reducer(store: AnyAction[] = []) {
+      return store
+    }
+  })
+
+  beforeEach(() => {
+    store = configureStore({
+      reducer(store: AnyAction[] = [], action) {
+        return [...store, action]
+      }
+    })
+  })
+
+  test('normal usage', async () => {
+    await store.dispatch(asyncThunk({}))
+    expect(store.getState()).toEqual([
+      expect.any(Object),
+      expect.objectContaining({ type: 'test/pending' }),
+      expect.objectContaining({ type: 'test/fulfilled' })
+    ])
+  })
+
+  test('abort after dispatch', async () => {
+    const promise = store.dispatch(asyncThunk({}))
+    promise.abort('AbortReason')
+    const result = await promise
+    const expectedAbortedAction = {
+      type: 'test/rejected',
+      error: {
+        message: 'AbortReason',
+        name: 'AbortError'
+      },
+      meta: { aborted: true }
+    }
+    // abortedAction with reason is dispatched after test/pending is dispatched
+    expect(store.getState()).toMatchObject([
+      {},
+      { type: 'test/pending' },
+      expectedAbortedAction
+    ])
+
+    // same abortedAction is returned, but with the AbortError from the abortablePayloadCreator
+    expect(result).toMatchObject(expectedAbortedAction)
+
+    // calling unwrapResult on the returned object re-throws the error from the abortablePayloadCreator
+    expect(() => unwrapResult(result)).toThrowError(
+      expect.objectContaining(expectedAbortedAction.error)
+    )
+  })
+
+  test('even when the payloadCreator does not directly support the signal, no further actions are dispatched', async () => {
+    const unawareAsyncThunk = createAsyncThunk('unaware', async () => {
+      await new Promise(resolve => setTimeout(resolve, 100))
+      return 'finished'
+    })
+
+    const promise = store.dispatch(unawareAsyncThunk())
+    promise.abort('AbortReason')
+    const result = await promise
+
+    const expectedAbortedAction = {
+      type: 'unaware/rejected',
+      error: {
+        message: 'AbortReason',
+        name: 'AbortError'
+      }
+    }
+
+    // abortedAction with reason is dispatched after test/pending is dispatched
+    expect(store.getState()).toEqual([
+      expect.any(Object),
+      expect.objectContaining({ type: 'unaware/pending' }),
+      expect.objectContaining(expectedAbortedAction)
+    ])
+
+    // same abortedAction is returned, but with the AbortError from the abortablePayloadCreator
+    expect(result).toMatchObject(expectedAbortedAction)
+
+    // calling unwrapResult on the returned object re-throws the error from the abortablePayloadCreator
+    expect(() => unwrapResult(result)).toThrowError(
+      expect.objectContaining(expectedAbortedAction.error)
+    )
+  })
+
+  test('dispatch(asyncThunk) returns on abort and does not wait for the promiseProvider to finish', async () => {
+    let running = false
+    const longRunningAsyncThunk = createAsyncThunk('longRunning', async () => {
+      running = true
+      await new Promise(resolve => setTimeout(resolve, 30000))
+      running = false
+    })
+
+    const promise = store.dispatch(longRunningAsyncThunk())
+    expect(running).toBeTruthy()
+    promise.abort()
+    const result = await promise
+    expect(running).toBeTruthy()
+    expect(result).toMatchObject({
+      type: 'longRunning/rejected',
+      error: { message: 'Aborted', name: 'AbortError' },
+      meta: { aborted: true }
+    })
+  })
+
+  describe('behaviour with missing AbortController', () => {
+    let keepAbortController: typeof window['AbortController']
+    let freshlyLoadedModule: typeof import('./createAsyncThunk')
+    let restore: () => void
+    let nodeEnv: string
+
+    beforeEach(() => {
+      keepAbortController = window.AbortController
+      delete window.AbortController
+      jest.resetModules()
+      freshlyLoadedModule = require('./createAsyncThunk')
+      restore = mockConsole(createConsole())
+      nodeEnv = process.env.NODE_ENV!
+      process.env.NODE_ENV = 'development'
+    })
+
+    afterEach(() => {
+      process.env.NODE_ENV = nodeEnv
+      restore()
+      window.AbortController = keepAbortController
+      jest.resetModules()
+    })
+
+    test('calling `abort` on an asyncThunk works with a FallbackAbortController if no global abortController is not available', async () => {
+      const longRunningAsyncThunk = freshlyLoadedModule.createAsyncThunk(
+        'longRunning',
+        async () => {
+          await new Promise(resolve => setTimeout(resolve, 30000))
+        }
+      )
+
+      store.dispatch(longRunningAsyncThunk()).abort()
+      // should only log once, even if called twice
+      store.dispatch(longRunningAsyncThunk()).abort()
+
+      expect(getLog().log).toMatchInlineSnapshot(`
+        "This platform does not implement AbortController. 
+        If you want to use the AbortController to react to \`abort\` events, please consider importing a polyfill like 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'."
+      `)
+    })
+  })
+})
diff --git a/src/entities/entity_state.test.ts b/src/entities/entity_state.test.ts
new file mode 100644
index 00000000..6a5cfdcc
--- /dev/null
+++ b/src/entities/entity_state.test.ts
@@ -0,0 +1,81 @@
+import { createEntityAdapter, EntityAdapter } from './index'
+import { PayloadAction, createAction } from '../createAction'
+import { createSlice } from '../createSlice'
+import { BookModel } from './fixtures/book'
+
+describe('Entity State', () => {
+  let adapter: EntityAdapter<BookModel>
+
+  beforeEach(() => {
+    adapter = createEntityAdapter({
+      selectId: (book: BookModel) => book.id
+    })
+  })
+
+  it('should let you get the initial state', () => {
+    const initialState = adapter.getInitialState()
+
+    expect(initialState).toEqual({
+      ids: [],
+      entities: {}
+    })
+  })
+
+  it('should let you provide additional initial state properties', () => {
+    const additionalProperties = { isHydrated: true }
+
+    const initialState = adapter.getInitialState(additionalProperties)
+
+    expect(initialState).toEqual({
+      ...additionalProperties,
+      ids: [],
+      entities: {}
+    })
+  })
+
+  it('should allow methods to be passed as reducers', () => {
+    const upsertBook = createAction<BookModel>('otherBooks/upsert')
+
+    const booksSlice = createSlice({
+      name: 'books',
+      initialState: adapter.getInitialState(),
+      reducers: {
+        addOne: adapter.addOne,
+        removeOne(state, action: PayloadAction<string>) {
+          // TODO The nested `produce` calls don't mutate `state` here as I would have expected.
+          // TODO (note that `state` here is actually an Immer Draft<S>, from `createReducer`)
+          // TODO However, this works if we _return_ the new plain result value instead
+          // TODO See https://github.com/immerjs/immer/issues/533
+          const result = adapter.removeOne(state, action)
+          return result
+        }
+      },
+      extraReducers: builder => {
+        builder.addCase(upsertBook, (state, action) => {
+          return adapter.upsertOne(state, action)
+        })
+      }
+    })
+
+    const { addOne, removeOne } = booksSlice.actions
+    const { reducer } = booksSlice
+
+    const selectors = adapter.getSelectors()
+
+    const book1: BookModel = { id: 'a', title: 'First' }
+    const book1a: BookModel = { id: 'a', title: 'Second' }
+
+    const afterAddOne = reducer(undefined, addOne(book1))
+    expect(afterAddOne.entities[book1.id]).toBe(book1)
+
+    const afterRemoveOne = reducer(afterAddOne, removeOne(book1.id))
+    expect(afterRemoveOne.entities[book1.id]).toBeUndefined()
+    expect(selectors.selectTotal(afterRemoveOne)).toBe(0)
+
+    const afterUpsertFirst = reducer(afterRemoveOne, upsertBook(book1))
+    const afterUpsertSecond = reducer(afterUpsertFirst, upsertBook(book1a))
+
+    expect(afterUpsertSecond.entities[book1.id]).toEqual(book1a)
+    expect(selectors.selectTotal(afterUpsertSecond)).toBe(1)
+  })
+})
diff --git a/src/entities/sorted_state_adapter.test.ts b/src/entities/sorted_state_adapter.test.ts
new file mode 100644
index 00000000..8b45a788
--- /dev/null
+++ b/src/entities/sorted_state_adapter.test.ts
@@ -0,0 +1,383 @@
+import { EntityStateAdapter, EntityState } from './models'
+import { createEntityAdapter } from './create_adapter'
+import { createAction } from '../createAction'
+import {
+  BookModel,
+  TheGreatGatsby,
+  AClockworkOrange,
+  AnimalFarm
+} from './fixtures/book'
+
+describe('Sorted State Adapter', () => {
+  let adapter: EntityStateAdapter<BookModel>
+  let state: EntityState<BookModel>
+
+  beforeAll(() => {
+    //eslint-disable-next-line
+    Object.defineProperty(Array.prototype, 'unwantedField', {
+      enumerable: true,
+      configurable: true,
+      value: 'This should not appear anywhere'
+    })
+  })
+
+  afterAll(() => {
+    delete (Array.prototype as any).unwantedField
+  })
+
+  beforeEach(() => {
+    adapter = createEntityAdapter({
+      selectId: (book: BookModel) => book.id,
+      sortComparer: (a, b) => {
+        return a.title.localeCompare(b.title)
+      }
+    })
+
+    state = { ids: [], entities: {} }
+  })
+
+  it('should let you add one entity to the state', () => {
+    const withOneEntity = adapter.addOne(state, TheGreatGatsby)
+
+    expect(withOneEntity).toEqual({
+      ids: [TheGreatGatsby.id],
+      entities: {
+        [TheGreatGatsby.id]: TheGreatGatsby
+      }
+    })
+  })
+
+  it('should let you add one entity to the state as an FSA', () => {
+    const bookAction = createAction<BookModel>('books/add')
+    const withOneEntity = adapter.addOne(state, bookAction(TheGreatGatsby))
+
+    expect(withOneEntity).toEqual({
+      ids: [TheGreatGatsby.id],
+      entities: {
+        [TheGreatGatsby.id]: TheGreatGatsby
+      }
+    })
+  })
+
+  it('should not change state if you attempt to re-add an entity', () => {
+    const withOneEntity = adapter.addOne(state, TheGreatGatsby)
+
+    const readded = adapter.addOne(withOneEntity, TheGreatGatsby)
+
+    expect(readded).toBe(withOneEntity)
+  })
+
+  it('should let you add many entities to the state', () => {
+    const withOneEntity = adapter.addOne(state, TheGreatGatsby)
+
+    const withManyMore = adapter.addMany(withOneEntity, [
+      AClockworkOrange,
+      AnimalFarm
+    ])
+
+    expect(withManyMore).toEqual({
+      ids: [AClockworkOrange.id, AnimalFarm.id, TheGreatGatsby.id],
+      entities: {
+        [TheGreatGatsby.id]: TheGreatGatsby,
+        [AClockworkOrange.id]: AClockworkOrange,
+        [AnimalFarm.id]: AnimalFarm
+      }
+    })
+  })
+
+  it('should remove existing and add new ones on setAll', () => {
+    const withOneEntity = adapter.addOne(state, TheGreatGatsby)
+
+    const withAll = adapter.setAll(withOneEntity, [
+      AClockworkOrange,
+      AnimalFarm
+    ])
+
+    expect(withAll).toEqual({
+      ids: [AClockworkOrange.id, AnimalFarm.id],
+      entities: {
+        [AClockworkOrange.id]: AClockworkOrange,
+        [AnimalFarm.id]: AnimalFarm
+      }
+    })
+  })
+
+  it('should remove existing and add new ones on addAll (deprecated)', () => {
+    const withOneEntity = adapter.addOne(state, TheGreatGatsby)
+
+    const withAll = adapter.setAll(withOneEntity, [
+      AClockworkOrange,
+      AnimalFarm
+    ])
+
+    expect(withAll).toEqual({
+      ids: [AClockworkOrange.id, AnimalFarm.id],
+      entities: {
+        [AClockworkOrange.id]: AClockworkOrange,
+        [AnimalFarm.id]: AnimalFarm
+      }
+    })
+  })
+
+  it('should let you add remove an entity from the state', () => {
+    const withOneEntity = adapter.addOne(state, TheGreatGatsby)
+
+    const withoutOne = adapter.removeOne(withOneEntity, TheGreatGatsby.id)
+
+    expect(withoutOne).toEqual({
+      ids: [],
+      entities: {}
+    })
+  })
+
+  it('should let you remove many entities by id from the state', () => {
+    const withAll = adapter.setAll(state, [
+      TheGreatGatsby,
+      AClockworkOrange,
+      AnimalFarm
+    ])
+
+    const withoutMany = adapter.removeMany(withAll, [
+      TheGreatGatsby.id,
+      AClockworkOrange.id
+    ])
+
+    expect(withoutMany).toEqual({
+      ids: [AnimalFarm.id],
+      entities: {
+        [AnimalFarm.id]: AnimalFarm
+      }
+    })
+  })
+
+  it('should let you remove all entities from the state', () => {
+    const withAll = adapter.setAll(state, [
+      TheGreatGatsby,
+      AClockworkOrange,
+      AnimalFarm
+    ])
+
+    const withoutAll = adapter.removeAll(withAll)
+
+    expect(withoutAll).toEqual({
+      ids: [],
+      entities: {}
+    })
+  })
+
+  it('should let you update an entity in the state', () => {
+    const withOne = adapter.addOne(state, TheGreatGatsby)
+    const changes = { title: 'A New Hope' }
+
+    const withUpdates = adapter.updateOne(withOne, {
+      id: TheGreatGatsby.id,
+      changes
+    })
+
+    expect(withUpdates).toEqual({
+      ids: [TheGreatGatsby.id],
+      entities: {
+        [TheGreatGatsby.id]: {
+          ...TheGreatGatsby,
+          ...changes
+        }
+      }
+    })
+  })
+
+  it('should not change state if you attempt to update an entity that has not been added', () => {
+    const withUpdates = adapter.updateOne(state, {
+      id: TheGreatGatsby.id,
+      changes: { title: 'A New Title' }
+    })
+
+    expect(withUpdates).toBe(state)
+  })
+
+  it('Replaces an existing entity if you change the ID while updating', () => {
+    const withAdded = adapter.setAll(state, [
+      { id: 'a', title: 'First' },
+      { id: 'b', title: 'Second' },
+      { id: 'c', title: 'Third' }
+    ])
+
+    const withUpdated = adapter.updateOne(withAdded, {
+      id: 'b',
+      changes: {
+        id: 'c'
+      }
+    })
+
+    const { ids, entities } = withUpdated
+
+    expect(ids.length).toBe(2)
+    expect(entities.a).toBeTruthy()
+    expect(entities.b).not.toBeTruthy()
+    expect(entities.c).toBeTruthy()
+    expect(entities.c!.id).toBe('c')
+    expect(entities.c!.title).toBe('Second')
+  })
+
+  it('should not change ids state if you attempt to update an entity that does not impact sorting', () => {
+    const withAll = adapter.setAll(state, [
+      TheGreatGatsby,
+      AClockworkOrange,
+      AnimalFarm
+    ])
+    const changes = { title: 'The Great Gatsby II' }
+
+    const withUpdates = adapter.updateOne(withAll, {
+      id: TheGreatGatsby.id,
+      changes
+    })
+
+    expect(withAll.ids).toBe(withUpdates.ids)
+  })
+
+  it('should let you update the id of entity', () => {
+    const withOne = adapter.addOne(state, TheGreatGatsby)
+    const changes = { id: 'A New Id' }
+
+    const withUpdates = adapter.updateOne(withOne, {
+      id: TheGreatGatsby.id,
+      changes
+    })
+
+    expect(withUpdates).toEqual({
+      ids: [changes.id],
+      entities: {
+        [changes.id]: {
+          ...TheGreatGatsby,
+          ...changes
+        }
+      }
+    })
+  })
+
+  it('should resort correctly if same id but sort key update', () => {
+    const withAll = adapter.setAll(state, [
+      TheGreatGatsby,
+      AnimalFarm,
+      AClockworkOrange
+    ])
+    const changes = { title: 'A New Hope' }
+
+    const withUpdates = adapter.updateOne(withAll, {
+      id: TheGreatGatsby.id,
+      changes
+    })
+
+    expect(withUpdates).toEqual({
+      ids: [AClockworkOrange.id, TheGreatGatsby.id, AnimalFarm.id],
+      entities: {
+        [AClockworkOrange.id]: AClockworkOrange,
+        [TheGreatGatsby.id]: {
+          ...TheGreatGatsby,
+          ...changes
+        },
+        [AnimalFarm.id]: AnimalFarm
+      }
+    })
+  })
+
+  it('should resort correctly if the id and sort key update', () => {
+    const withOne = adapter.setAll(state, [
+      TheGreatGatsby,
+      AnimalFarm,
+      AClockworkOrange
+    ])
+    const changes = { id: 'A New Id', title: 'A New Hope' }
+
+    const withUpdates = adapter.updateOne(withOne, {
+      id: TheGreatGatsby.id,
+      changes
+    })
+
+    expect(withUpdates).toEqual({
+      ids: [AClockworkOrange.id, changes.id, AnimalFarm.id],
+      entities: {
+        [AClockworkOrange.id]: AClockworkOrange,
+        [changes.id]: {
+          ...TheGreatGatsby,
+          ...changes
+        },
+        [AnimalFarm.id]: AnimalFarm
+      }
+    })
+  })
+
+  it('should let you update many entities by id in the state', () => {
+    const firstChange = { title: 'Zack' }
+    const secondChange = { title: 'Aaron' }
+    const withMany = adapter.setAll(state, [TheGreatGatsby, AClockworkOrange])
+
+    const withUpdates = adapter.updateMany(withMany, [
+      { id: TheGreatGatsby.id, changes: firstChange },
+      { id: AClockworkOrange.id, changes: secondChange }
+    ])
+
+    expect(withUpdates).toEqual({
+      ids: [AClockworkOrange.id, TheGreatGatsby.id],
+      entities: {
+        [TheGreatGatsby.id]: {
+          ...TheGreatGatsby,
+          ...firstChange
+        },
+        [AClockworkOrange.id]: {
+          ...AClockworkOrange,
+          ...secondChange
+        }
+      }
+    })
+  })
+
+  it('should let you add one entity to the state with upsert()', () => {
+    const withOneEntity = adapter.upsertOne(state, TheGreatGatsby)
+    expect(withOneEntity).toEqual({
+      ids: [TheGreatGatsby.id],
+      entities: {
+        [TheGreatGatsby.id]: TheGreatGatsby
+      }
+    })
+  })
+
+  it('should let you update an entity in the state with upsert()', () => {
+    const withOne = adapter.addOne(state, TheGreatGatsby)
+    const changes = { title: 'A New Hope' }
+
+    const withUpdates = adapter.upsertOne(withOne, {
+      ...TheGreatGatsby,
+      ...changes
+    })
+    expect(withUpdates).toEqual({
+      ids: [TheGreatGatsby.id],
+      entities: {
+        [TheGreatGatsby.id]: {
+          ...TheGreatGatsby,
+          ...changes
+        }
+      }
+    })
+  })
+
+  it('should let you upsert many entities in the state', () => {
+    const firstChange = { title: 'Zack' }
+    const withMany = adapter.setAll(state, [TheGreatGatsby])
+
+    const withUpserts = adapter.upsertMany(withMany, [
+      { ...TheGreatGatsby, ...firstChange },
+      AClockworkOrange
+    ])
+
+    expect(withUpserts).toEqual({
+      ids: [AClockworkOrange.id, TheGreatGatsby.id],
+      entities: {
+        [TheGreatGatsby.id]: {
+          ...TheGreatGatsby,
+          ...firstChange
+        },
+        [AClockworkOrange.id]: AClockworkOrange
+      }
+    })
+  })
+})
diff --git a/src/entities/state_adapter.test.ts b/src/entities/state_adapter.test.ts
new file mode 100644
index 00000000..9c0d3226
--- /dev/null
+++ b/src/entities/state_adapter.test.ts
@@ -0,0 +1,60 @@
+import { createEntityAdapter, EntityAdapter } from './index'
+import { PayloadAction } from '../createAction'
+import { configureStore } from '../configureStore'
+import { createSlice } from '../createSlice'
+import { BookModel } from './fixtures/book'
+
+describe('createStateOperator', () => {
+  let adapter: EntityAdapter<BookModel>
+
+  beforeEach(() => {
+    adapter = createEntityAdapter({
+      selectId: (book: BookModel) => book.id
+    })
+  })
+  it('Correctly mutates a draft state when inside `createNextState', () => {
+    const booksSlice = createSlice({
+      name: 'books',
+      initialState: adapter.getInitialState(),
+      reducers: {
+        // We should be able to call an adapter method as a mutating helper in a larger reducer
+        addOne(state, action: PayloadAction<BookModel>) {
+          // Originally, having nested `produce` calls don't mutate `state` here as I would have expected.
+          // (note that `state` here is actually an Immer Draft<S>, from `createReducer`)
+          // One woarkound was to return the new plain result value instead
+          // See https://github.com/immerjs/immer/issues/533
+          // However, after tweaking `createStateOperator` to check if the argument is a draft,
+          // we can just treat the operator as strictly mutating, without returning a result,
+          // and the result should be correct.
+          const result = adapter.addOne(state, action)
+          expect(result.ids.length).toBe(1)
+          //Deliberately _don't_ return result
+        },
+        // We should also be able to pass them individually as case reducers
+        addAnother: adapter.addOne
+      }
+    })
+
+    const { addOne, addAnother } = booksSlice.actions
+
+    const store = configureStore({
+      reducer: {
+        books: booksSlice.reducer
+      }
+    })
+
+    const book1: BookModel = { id: 'a', title: 'First' }
+    store.dispatch(addOne(book1))
+
+    const state1 = store.getState()
+    expect(state1.books.ids.length).toBe(1)
+    expect(state1.books.entities['a']).toBe(book1)
+
+    const book2: BookModel = { id: 'b', title: 'Second' }
+    store.dispatch(addAnother(book2))
+
+    const state2 = store.getState()
+    expect(state2.books.ids.length).toBe(2)
+    expect(state2.books.entities['b']).toBe(book2)
+  })
+})
diff --git a/src/entities/state_selectors.test.ts b/src/entities/state_selectors.test.ts
new file mode 100644
index 00000000..7b8c11b7
--- /dev/null
+++ b/src/entities/state_selectors.test.ts
@@ -0,0 +1,128 @@
+import { createEntityAdapter, EntityAdapter, EntityState } from './index'
+import { EntitySelectors } from './models'
+import {
+  BookModel,
+  AClockworkOrange,
+  AnimalFarm,
+  TheGreatGatsby
+} from './fixtures/book'
+import { createSelector, Selector } from 'reselect'
+
+describe('Entity State Selectors', () => {
+  describe('Composed Selectors', () => {
+    interface State {
+      books: EntityState<BookModel>
+    }
+
+    let adapter: EntityAdapter<BookModel>
+    let selectors: EntitySelectors<BookModel, State>
+    let state: State
+
+    beforeEach(() => {
+      adapter = createEntityAdapter({
+        selectId: (book: BookModel) => book.id
+      })
+
+      state = {
+        books: adapter.setAll(adapter.getInitialState(), [
+          AClockworkOrange,
+          AnimalFarm,
+          TheGreatGatsby
+        ])
+      }
+
+      selectors = adapter.getSelectors((state: State) => state.books)
+    })
+
+    it('should create a selector for selecting the ids', () => {
+      const ids = selectors.selectIds(state)
+
+      expect(ids).toEqual(state.books.ids)
+    })
+
+    it('should create a selector for selecting the entities', () => {
+      const entities = selectors.selectEntities(state)
+
+      expect(entities).toEqual(state.books.entities)
+    })
+
+    it('should create a selector for selecting the list of models', () => {
+      const models = selectors.selectAll(state)
+
+      expect(models).toEqual([AClockworkOrange, AnimalFarm, TheGreatGatsby])
+    })
+
+    it('should create a selector for selecting the count of models', () => {
+      const total = selectors.selectTotal(state)
+
+      expect(total).toEqual(3)
+    })
+
+    it('should create a selector for selecting a single item by ID', () => {
+      const first = selectors.selectById(state, AClockworkOrange.id)
+      expect(first).toBe(AClockworkOrange)
+      const second = selectors.selectById(state, AnimalFarm.id)
+      expect(second).toBe(AnimalFarm)
+    })
+  })
+
+  describe('Uncomposed Selectors', () => {
+    type State = EntityState<BookModel>
+
+    let adapter: EntityAdapter<BookModel>
+    let selectors: EntitySelectors<BookModel, EntityState<BookModel>>
+    let state: State
+
+    beforeEach(() => {
+      adapter = createEntityAdapter({
+        selectId: (book: BookModel) => book.id
+      })
+
+      state = adapter.setAll(adapter.getInitialState(), [
+        AClockworkOrange,
+        AnimalFarm,
+        TheGreatGatsby
+      ])
+
+      selectors = adapter.getSelectors()
+    })
+
+    it('should create a selector for selecting the ids', () => {
+      const ids = selectors.selectIds(state)
+
+      expect(ids).toEqual(state.ids)
+    })
+
+    it('should create a selector for selecting the entities', () => {
+      const entities = selectors.selectEntities(state)
+
+      expect(entities).toEqual(state.entities)
+    })
+
+    it('should type single entity from Dictionary as entity type or undefined', () => {
+      const singleEntity: Selector<
+        EntityState<BookModel>,
+        BookModel | undefined
+      > = createSelector(selectors.selectEntities, entities => entities[0])
+    })
+
+    it('should create a selector for selecting the list of models', () => {
+      const models = selectors.selectAll(state)
+
+      expect(models).toEqual([AClockworkOrange, AnimalFarm, TheGreatGatsby])
+    })
+
+    it('should create a selector for selecting the count of models', () => {
+      const total = selectors.selectTotal(state)
+
+      expect(total).toEqual(3)
+    })
+
+    it('should create a selector for selecting a single item by ID', () => {
+      const first = selectors.selectById(state, AClockworkOrange.id)
+      expect(first).toBe(AClockworkOrange)
+      const second = selectors.selectById(state, AnimalFarm.id)
+      expect(second).toBe(AnimalFarm)
+    })
+  })
+})
diff --git a/src/entities/unsorted_state_adapter.test.ts b/src/entities/unsorted_state_adapter.test.ts
new file mode 100644
index 00000000..c4448337
--- /dev/null
+++ b/src/entities/unsorted_state_adapter.test.ts
@@ -0,0 +1,302 @@
+import { EntityStateAdapter, EntityState } from './models'
+import { createEntityAdapter } from './create_adapter'
+import {
+  BookModel,
+  TheGreatGatsby,
+  AClockworkOrange,
+  AnimalFarm
+} from './fixtures/book'
+
+describe('Unsorted State Adapter', () => {
+  let adapter: EntityStateAdapter<BookModel>
+  let state: EntityState<BookModel>
+
+  beforeAll(() => {
+    //eslint-disable-next-line
+    Object.defineProperty(Array.prototype, 'unwantedField', {
+      enumerable: true,
+      configurable: true,
+      value: 'This should not appear anywhere'
+    })
+  })
+
+  afterAll(() => {
+    delete (Array.prototype as any).unwantedField
+  })
+
+  beforeEach(() => {
+    adapter = createEntityAdapter({
+      selectId: (book: BookModel) => book.id
+    })
+
+    state = { ids: [], entities: {} }
+  })
+
+  it('should let you add one entity to the state', () => {
+    const withOneEntity = adapter.addOne(state, TheGreatGatsby)
+
+    expect(withOneEntity).toEqual({
+      ids: [TheGreatGatsby.id],
+      entities: {
+        [TheGreatGatsby.id]: TheGreatGatsby
+      }
+    })
+  })
+
+  it('should not change state if you attempt to re-add an entity', () => {
+    const withOneEntity = adapter.addOne(state, TheGreatGatsby)
+
+    const readded = adapter.addOne(withOneEntity, TheGreatGatsby)
+
+    expect(readded).toBe(withOneEntity)
+  })
+
+  it('should let you add many entities to the state', () => {
+    const withOneEntity = adapter.addOne(state, TheGreatGatsby)
+
+    const withManyMore = adapter.addMany(withOneEntity, [
+      AClockworkOrange,
+      AnimalFarm
+    ])
+
+    expect(withManyMore).toEqual({
+      ids: [TheGreatGatsby.id, AClockworkOrange.id, AnimalFarm.id],
+      entities: {
+        [TheGreatGatsby.id]: TheGreatGatsby,
+        [AClockworkOrange.id]: AClockworkOrange,
+        [AnimalFarm.id]: AnimalFarm
+      }
+    })
+  })
+
+  it('should remove existing and add new ones on setAll', () => {
+    const withOneEntity = adapter.addOne(state, TheGreatGatsby)
+
+    const withAll = adapter.setAll(withOneEntity, [
+      AClockworkOrange,
+      AnimalFarm
+    ])
+
+    expect(withAll).toEqual({
+      ids: [AClockworkOrange.id, AnimalFarm.id],
+      entities: {
+        [AClockworkOrange.id]: AClockworkOrange,
+        [AnimalFarm.id]: AnimalFarm
+      }
+    })
+  })
+
+  it('should let you add remove an entity from the state', () => {
+    const withOneEntity = adapter.addOne(state, TheGreatGatsby)
+
+    const withoutOne = adapter.removeOne(state, TheGreatGatsby.id)
+
+    expect(withoutOne).toEqual({
+      ids: [],
+      entities: {}
+    })
+  })
+
+  it('should let you remove many entities by id from the state', () => {
+    const withAll = adapter.setAll(state, [
+      TheGreatGatsby,
+      AClockworkOrange,
+      AnimalFarm
+    ])
+
+    const withoutMany = adapter.removeMany(withAll, [
+      TheGreatGatsby.id,
+      AClockworkOrange.id
+    ])
+
+    expect(withoutMany).toEqual({
+      ids: [AnimalFarm.id],
+      entities: {
+        [AnimalFarm.id]: AnimalFarm
+      }
+    })
+  })
+
+  it('should let you remove all entities from the state', () => {
+    const withAll = adapter.setAll(state, [
+      TheGreatGatsby,
+      AClockworkOrange,
+      AnimalFarm
+    ])
+
+    const withoutAll = adapter.removeAll(withAll)
+
+    expect(withoutAll).toEqual({
+      ids: [],
+      entities: {}
+    })
+  })
+
+  it('should let you update an entity in the state', () => {
+    const withOne = adapter.addOne(state, TheGreatGatsby)
+    const changes = { title: 'A New Hope' }
+
+    const withUpdates = adapter.updateOne(withOne, {
+      id: TheGreatGatsby.id,
+      changes
+    })
+
+    expect(withUpdates).toEqual({
+      ids: [TheGreatGatsby.id],
+      entities: {
+        [TheGreatGatsby.id]: {
+          ...TheGreatGatsby,
+          ...changes
+        }
+      }
+    })
+  })
+
+  it('should not change state if you attempt to update an entity that has not been added', () => {
+    const withUpdates = adapter.updateOne(state, {
+      id: TheGreatGatsby.id,
+      changes: { title: 'A New Title' }
+    })
+
+    expect(withUpdates).toBe(state)
+  })
+
+  it('should not change ids state if you attempt to update an entity that has already been added', () => {
+    const withOne = adapter.addOne(state, TheGreatGatsby)
+    const changes = { title: 'A New Hope' }
+
+    const withUpdates = adapter.updateOne(withOne, {
+      id: TheGreatGatsby.id,
+      changes
+    })
+
+    expect(withOne.ids).toBe(withUpdates.ids)
+  })
+
+  it('should let you update the id of entity', () => {
+    const withOne = adapter.addOne(state, TheGreatGatsby)
+    const changes = { id: 'A New Id' }
+
+    const withUpdates = adapter.updateOne(withOne, {
+      id: TheGreatGatsby.id,
+      changes
+    })
+
+    expect(withUpdates).toEqual({
+      ids: [changes.id],
+      entities: {
+        [changes.id]: {
+          ...TheGreatGatsby,
+          ...changes
+        }
+      }
+    })
+  })
+
+  it('should let you update many entities by id in the state', () => {
+    const firstChange = { title: 'First Change' }
+    const secondChange = { title: 'Second Change' }
+    const withMany = adapter.setAll(state, [TheGreatGatsby, AClockworkOrange])
+
+    const withUpdates = adapter.updateMany(withMany, [
+      { id: TheGreatGatsby.id, changes: firstChange },
+      { id: AClockworkOrange.id, changes: secondChange }
+    ])
+
+    expect(withUpdates).toEqual({
+      ids: [TheGreatGatsby.id, AClockworkOrange.id],
+      entities: {
+        [TheGreatGatsby.id]: {
+          ...TheGreatGatsby,
+          ...firstChange
+        },
+        [AClockworkOrange.id]: {
+          ...AClockworkOrange,
+          ...secondChange
+        }
+      }
+    })
+  })
+
+  it("doesn't break when multiple renames of one item occur", () => {
+    const withA = adapter.addOne(state, { id: 'a', title: 'First' })
+
+    const withUpdates = adapter.updateMany(withA, [
+      { id: 'a', changes: { id: 'b' } },
+      { id: 'a', changes: { id: 'c' } }
+    ])
+
+    const { ids, entities } = withUpdates
+
+    /*
+      Original code failed with a mish-mash of values, like:
+
+      {
+        ids: [ 'c' ],
+        entities: { b: { id: 'b', title: 'First' }, c: { id: 'c' } }
+      }
+
+      We now expect that only 'c' will be left:
+
+      { 
+        ids: [ 'c' ], 
+        entities: { c: { id: 'c', title: 'First' } } 
+      }
+    */
+    expect(ids.length).toBe(1)
+    expect(ids).toEqual(['c'])
+    expect(entities.a).toBeFalsy()
+    expect(entities.b).toBeFalsy()
+    expect(entities.c).toBeTruthy()
+  })
+
+  it('should let you add one entity to the state with upsert()', () => {
+    const withOneEntity = adapter.upsertOne(state, TheGreatGatsby)
+    expect(withOneEntity).toEqual({
+      ids: [TheGreatGatsby.id],
+      entities: {
+        [TheGreatGatsby.id]: TheGreatGatsby
+      }
+    })
+  })
+
+  it('should let you update an entity in the state with upsert()', () => {
+    const withOne = adapter.addOne(state, TheGreatGatsby)
+    const changes = { title: 'A New Hope' }
+
+    const withUpdates = adapter.upsertOne(withOne, {
+      ...TheGreatGatsby,
+      ...changes
+    })
+    expect(withUpdates).toEqual({
+      ids: [TheGreatGatsby.id],
+      entities: {
+        [TheGreatGatsby.id]: {
+          ...TheGreatGatsby,
+          ...changes
+        }
+      }
+    })
+  })
+
+  it('should let you upsert many entities in the state', () => {
+    const firstChange = { title: 'First Change' }
+    const withMany = adapter.setAll(state, [TheGreatGatsby])
+
+    const withUpserts = adapter.upsertMany(withMany, [
+      { ...TheGreatGatsby, ...firstChange },
+      AClockworkOrange
+    ])
+
+    expect(withUpserts).toEqual({
+      ids: [TheGreatGatsby.id, AClockworkOrange.id],
+      entities: {
+        [TheGreatGatsby.id]: {
+          ...TheGreatGatsby,
+          ...firstChange
+        },
+        [AClockworkOrange.id]: AClockworkOrange
+      }
+    })
+  })
+})
diff --git a/src/immutableStateInvariantMiddleware.test.ts b/src/immutableStateInvariantMiddleware.test.ts
new file mode 100644
index 00000000..104e677d
--- /dev/null
+++ b/src/immutableStateInvariantMiddleware.test.ts
@@ -0,0 +1,483 @@
+import { Store, MiddlewareAPI, Dispatch } from 'redux'
+import {
+  createImmutableStateInvariantMiddleware,
+  isImmutableDefault,
+  trackForMutations,
+  ImmutableStateInvariantMiddlewareOptions
+} from './immutableStateInvariantMiddleware'
+import { mockConsole, createConsole, getLog } from 'console-testing-library'
+
+describe('createImmutableStateInvariantMiddleware', () => {
+  let state: { foo: { bar: number[]; baz: string } }
+  const getState: Store['getState'] = () => state
+
+  function middleware(options: ImmutableStateInvariantMiddlewareOptions = {}) {
+    return createImmutableStateInvariantMiddleware(options)({
+      getState
+    } as MiddlewareAPI)
+  }
+
+  beforeEach(() => {
+    state = { foo: { bar: [2, 3, 4], baz: 'baz' } }
+  })
+
+  it('sends the action through the middleware chain', () => {
+    const next: Dispatch = action => ({ ...action, returned: true })
+    const dispatch = middleware()(next)
+
+    expect(dispatch({ type: 'SOME_ACTION' })).toEqual({
+      type: 'SOME_ACTION',
+      returned: true
+    })
+  })
+
+  it('throws if mutating inside the dispatch', () => {
+    const next: Dispatch = action => {
+      state.foo.bar.push(5)
+      return action
+    }
+
+    const dispatch = middleware()(next)
+
+    expect(() => {
+      dispatch({ type: 'SOME_ACTION' })
+    }).toThrow(new RegExp('foo\\.bar\\.3'))
+  })
+
+  it('throws if mutating between dispatches', () => {
+    const next: Dispatch = action => action
+
+    const dispatch = middleware()(next)
+
+    dispatch({ type: 'SOME_ACTION' })
+    state.foo.bar.push(5)
+    expect(() => {
+      dispatch({ type: 'SOME_OTHER_ACTION' })
+    }).toThrow(new RegExp('foo\\.bar\\.3'))
+  })
+
+  it('does not throw if not mutating inside the dispatch', () => {
+    const next: Dispatch = action => {
+      state = { ...state, foo: { ...state.foo, baz: 'changed!' } }
+      return action
+    }
+
+    const dispatch = middleware()(next)
+
+    expect(() => {
+      dispatch({ type: 'SOME_ACTION' })
+    }).not.toThrow()
+  })
+
+  it('does not throw if not mutating between dispatches', () => {
+    const next: Dispatch = action => action
+
+    const dispatch = middleware()(next)
+
+    dispatch({ type: 'SOME_ACTION' })
+    state = { ...state, foo: { ...state.foo, baz: 'changed!' } }
+    expect(() => {
+      dispatch({ type: 'SOME_OTHER_ACTION' })
+    }).not.toThrow()
+  })
+
+  it('works correctly with circular references', () => {
+    const next: Dispatch = action => action
+
+    const dispatch = middleware()(next)
+
+    let x: any = {}
+    let y: any = {}
+    x.y = y
+    y.x = x
+
+    expect(() => {
+      dispatch({ type: 'SOME_ACTION', x })
+    }).not.toThrow()
+  })
+
+  it('respects "isImmutable" option', function() {
+    const isImmutable = (value: any) => true
+    const next: Dispatch = action => {
+      state.foo.bar.push(5)
+      return action
+    }
+
+    const dispatch = middleware({ isImmutable })(next)
+
+    expect(() => {
+      dispatch({ type: 'SOME_ACTION' })
+    }).not.toThrow()
+  })
+
+  it('respects "ignore" option', () => {
+    const next: Dispatch = action => {
+      state.foo.bar.push(5)
+      return action
+    }
+
+    const dispatch = middleware({ ignoredPaths: ['foo.bar'] })(next)
+
+    expect(() => {
+      dispatch({ type: 'SOME_ACTION' })
+    }).not.toThrow()
+  })
+
+  it('Should print a warning if execution takes too long', () => {
+    state.foo.bar = new Array(10000).fill({ value: 'more' })
+
+    const next: Dispatch = action => action
+
+    const dispatch = middleware({ warnAfter: 4 })(next)
+
+    const restore = mockConsole(createConsole())
+    try {
+      dispatch({ type: 'SOME_ACTION' })
+      expect(getLog().log).toMatch(
+        /^ImmutableStateInvariantMiddleware took \d*ms, which is more than the warning threshold of 4ms./
+      )
+    } finally {
+      restore()
+    }
+  })
+
+  it('Should not print a warning if "next" takes too long', () => {
+    const next: Dispatch = action => {
+      const started = Date.now()
+      while (Date.now() - started < 8) {}
+      return action
+    }
+
+    const dispatch = middleware({ warnAfter: 4 })(next)
+
+    const restore = mockConsole(createConsole())
+    try {
+      dispatch({ type: 'SOME_ACTION' })
+      expect(getLog().log).toEqual('')
+    } finally {
+      restore()
+    }
+  })
+})
+
+describe('trackForMutations', () => {
+  function testCasesForMutation(spec: any) {
+    it('returns true and the mutated path', () => {
+      const state = spec.getState()
+      const options = spec.middlewareOptions || {}
+      const { isImmutable = isImmutableDefault, ignoredPaths } = options
+      const tracker = trackForMutations(isImmutable, ignoredPaths, state)
+      const newState = spec.fn(state)
+
+      expect(tracker.detectMutations()).toEqual({
+        wasMutated: true,
+        path: spec.path
+      })
+    })
+  }
+
+  function testCasesForNonMutation(spec: any) {
+    it('returns false', () => {
+      const state = spec.getState()
+      const options = spec.middlewareOptions || {}
+      const { isImmutable = isImmutableDefault, ignoredPaths } = options
+      const tracker = trackForMutations(isImmutable, ignoredPaths, state)
+      const newState = spec.fn(state)
+
+      expect(tracker.detectMutations()).toEqual({ wasMutated: false })
+    })
+  }
+
+  interface TestConfig {
+    getState: Store['getState']
+    fn: (s: any) => typeof s | object
+    middlewareOptions?: ImmutableStateInvariantMiddlewareOptions
+    path?: string[]
+  }
+
+  const mutations: Record<string, TestConfig> = {
+    'adding to nested array': {
+      getState: () => ({
+        foo: {
+          bar: [2, 3, 4],
+          baz: 'baz'
+        },
+        stuff: []
+      }),
+      fn: s => {
+        s.foo.bar.push(5)
+        return s
+      },
+      path: ['foo', 'bar', '3']
+    },
+    'adding to nested array and setting new root object': {
+      getState: () => ({
+        foo: {
+          bar: [2, 3, 4],
+          baz: 'baz'
+        },
+        stuff: []
+      }),
+      fn: s => {
+        s.foo.bar.push(5)
+        return { ...s }
+      },
+      path: ['foo', 'bar', '3']
+    },
+    'changing nested string': {
+      getState: () => ({
+        foo: {
+          bar: [2, 3, 4],
+          baz: 'baz'
+        },
+        stuff: []
+      }),
+      fn: s => {
+        s.foo.baz = 'changed!'
+        return s
+      },
+      path: ['foo', 'baz']
+    },
+    'removing nested state': {
+      getState: () => ({
+        foo: {
+          bar: [2, 3, 4],
+          baz: 'baz'
+        },
+        stuff: []
+      }),
+      fn: s => {
+        delete s.foo
+        return s
+      },
+      path: ['foo']
+    },
+    'adding to array': {
+      getState: () => ({
+        foo: {
+          bar: [2, 3, 4],
+          baz: 'baz'
+        },
+        stuff: []
+      }),
+      fn: s => {
+        s.stuff.push(1)
+        return s
+      },
+      path: ['stuff', '0']
+    },
+    'adding object to array': {
+      getState: () => ({
+        stuff: []
+      }),
+      fn: s => {
+        s.stuff.push({ foo: 1, bar: 2 })
+        return s
+      },
+      path: ['stuff', '0']
+    },
+    'mutating previous state and returning new state': {
+      getState: () => ({ counter: 0 }),
+      fn: s => {
+        s.mutation = true
+        return { ...s, counter: s.counter + 1 }
+      },
+      path: ['mutation']
+    },
+    'mutating previous state with non immutable type and returning new state': {
+      getState: () => ({ counter: 0 }),
+      fn: s => {
+        s.mutation = [1, 2, 3]
+        return { ...s, counter: s.counter + 1 }
+      },
+      path: ['mutation']
+    },
+    'mutating previous state with non immutable type and returning new state without that property': {
+      getState: () => ({ counter: 0 }),
+      fn: s => {
+        s.mutation = [1, 2, 3]
+        return { counter: s.counter + 1 }
+      },
+      path: ['mutation']
+    },
+    'mutating previous state with non immutable type and returning new simple state': {
+      getState: () => ({ counter: 0 }),
+      fn: s => {
+        s.mutation = [1, 2, 3]
+        return 1
+      },
+      path: ['mutation']
+    },
+    'mutating previous state by deleting property and returning new state without that property': {
+      getState: () => ({ counter: 0, toBeDeleted: true }),
+      fn: s => {
+        delete s.toBeDeleted
+        return { counter: s.counter + 1 }
+      },
+      path: ['toBeDeleted']
+    },
+    'mutating previous state by deleting nested property': {
+      getState: () => ({ nested: { counter: 0, toBeDeleted: true }, foo: 1 }),
+      fn: s => {
+        delete s.nested.toBeDeleted
+        return { nested: { counter: s.counter + 1 } }
+      },
+      path: ['nested', 'toBeDeleted']
+    },
+    'update reference': {
+      getState: () => ({ foo: {} }),
+      fn: s => {
+        s.foo = {}
+        return s
+      },
+      path: ['foo']
+    },
+    'cannot ignore root state': {
+      getState: () => ({ foo: {} }),
+      fn: s => {
+        s.foo = {}
+        return s
+      },
+      middlewareOptions: {
+        ignoredPaths: ['']
+      },
+      path: ['foo']
+    },
+    'catching state mutation in non-ignored branch': {
+      getState: () => ({
+        foo: {
+          bar: [1, 2]
+        },
+        boo: {
+          yah: [1, 2]
+        }
+      }),
+      fn: s => {
+        s.foo.bar.push(3)
+        s.boo.yah.push(3)
+        return s
+      },
+      middlewareOptions: {
+        ignoredPaths: ['foo']
+      },
+      path: ['boo', 'yah', '2']
+    }
+  }
+
+  Object.keys(mutations).forEach(mutationDesc => {
+    describe(mutationDesc, () => {
+      testCasesForMutation(mutations[mutationDesc])
+    })
+  })
+
+  const nonMutations: Record<string, TestConfig> = {
+    'not doing anything': {
+      getState: () => ({ a: 1, b: 2 }),
+      fn: s => s
+    },
+    'from undefined to something': {
+      getState: () => undefined,
+      fn: s => ({ foo: 'bar' })
+    },
+    'returning same state': {
+      getState: () => ({
+        foo: {
+          bar: [2, 3, 4],
+          baz: 'baz'
+        },
+        stuff: []
+      }),
+      fn: s => s
+    },
+    'returning a new state object with nested new string': {
+      getState: () => ({
+        foo: {
+          bar: [2, 3, 4],
+          baz: 'baz'
+        },
+        stuff: []
+      }),
+      fn: s => {
+        return { ...s, foo: { ...s.foo, baz: 'changed!' } }
+      }
+    },
+    'returning a new state object with nested new array': {
+      getState: () => ({
+        foo: {
+          bar: [2, 3, 4],
+          baz: 'baz'
+        },
+        stuff: []
+      }),
+      fn: s => {
+        return { ...s, foo: { ...s.foo, bar: [...s.foo.bar, 5] } }
+      }
+    },
+    'removing nested state': {
+      getState: () => ({
+        foo: {
+          bar: [2, 3, 4],
+          baz: 'baz'
+        },
+        stuff: []
+      }),
+      fn: s => {
+        return { ...s, foo: {} }
+      }
+    },
+    'having a NaN in the state': {
+      getState: () => ({ a: NaN, b: Number.NaN }),
+      fn: s => s
+    },
+    'ignoring branches from mutation detection': {
+      getState: () => ({
+        foo: {
+          bar: 'bar'
+        }
+      }),
+      fn: s => {
+        s.foo.bar = 'baz'
+        return s
+      },
+      middlewareOptions: {
+        ignoredPaths: ['foo']
+      }
+    },
+    'ignoring nested branches from mutation detection': {
+      getState: () => ({
+        foo: {
+          bar: [1, 2],
+          boo: {
+            yah: [1, 2]
+          }
+        }
+      }),
+      fn: s => {
+        s.foo.bar.push(3)
+        s.foo.boo.yah.push(3)
+        return s
+      },
+      middlewareOptions: {
+        ignoredPaths: ['foo.bar', 'foo.boo.yah']
+      }
+    },
+    'ignoring nested array indices from mutation detection': {
+      getState: () => ({
+        stuff: [{ a: 1 }, { a: 2 }]
+      }),
+      fn: s => {
+        s.stuff[1].a = 3
+        return s
+      },
+      middlewareOptions: {
+        ignoredPaths: ['stuff.1']
+      }
+    }
+  }
+
+  Object.keys(nonMutations).forEach(nonMutationDesc => {
+    describe(nonMutationDesc, () => {
+      testCasesForNonMutation(nonMutations[nonMutationDesc])
+    })
+  })
+})
diff --git a/src/serializableStateInvariantMiddleware.test.ts b/src/serializableStateInvariantMiddleware.test.ts
index 5044f7da..c435b659 100644
--- a/src/serializableStateInvariantMiddleware.test.ts
+++ b/src/serializableStateInvariantMiddleware.test.ts
@@ -385,4 +385,74 @@ describe('serializableStateInvariantMiddleware', () => {
       (See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
     `)
   })
+
+  it('should not check serializability for meta.args by default', () => {
+    const badValue = new Map()
+
+    const reducer: Reducer = (state = 42, action) => {
+      return state
+    }
+
+    const serializableStateInvariantMiddleware = createSerializableStateInvariantMiddleware()
+
+    const store = configureStore({
+      reducer: {
+        testSlice: reducer
+      },
+      middleware: [serializableStateInvariantMiddleware]
+    })
+
+    store.dispatch({ type: 'testAction', meta: { args: { badValue } } })
+
+    const { log } = getLog()
+    expect(log).toBe('')
+    const q = 42
+  })
+
+  it('Should print a warning if execution takes too long', () => {
+    const reducer: Reducer = (state = 42, action) => {
+      return state
+    }
+
+    const serializableStateInvariantMiddleware = createSerializableStateInvariantMiddleware(
+      { warnAfter: 4 }
+    )
+
+    const store = configureStore({
+      reducer: {
+        testSlice: reducer
+      },
+      middleware: [serializableStateInvariantMiddleware]
+    })
+
+    store.dispatch({
+      type: 'SOME_ACTION',
+      payload: new Array(10000).fill({ value: 'more' })
+    })
+    expect(getLog().log).toMatch(
+      /^SerializableStateInvariantMiddleware took \d*ms, which is more than the warning threshold of 4ms./
+    )
+  })
+
+  it('Should not print a warning if "reducer" takes too long', () => {
+    const reducer: Reducer = (state = 42, action) => {
+      const started = Date.now()
+      while (Date.now() - started < 8) {}
+      return state
+    }
+
+    const serializableStateInvariantMiddleware = createSerializableStateInvariantMiddleware(
+      { warnAfter: 4 }
+    )
+
+    const store = configureStore({
+      reducer: {
+        testSlice: reducer
+      },
+      middleware: [serializableStateInvariantMiddleware]
+    })
+
+    store.dispatch({ type: 'SOME_ACTION' })
+    expect(getLog().log).toMatch('')
+  })
 })
diff --git a/type-tests/files/createAction.typetest.ts b/type-tests/files/createAction.typetest.ts
index 702913a6..e702070b 100644
--- a/type-tests/files/createAction.typetest.ts
+++ b/type-tests/files/createAction.typetest.ts
@@ -73,9 +73,9 @@ function expectType<T>(p: T): T {
     { type: 'action' }
   ) as PayloadActionCreator<number | undefined>
 
-  expectType<PayloadAction<number>>(actionCreator(1))
-  expectType<PayloadAction<undefined>>(actionCreator())
-  expectType<PayloadAction<undefined>>(actionCreator(undefined))
+  expectType<PayloadAction<number | undefined>>(actionCreator(1))
+  expectType<PayloadAction<number | undefined>>(actionCreator())
+  expectType<PayloadAction<number | undefined>>(actionCreator(undefined))
 
   // typings:expect-error
   expectType<PayloadAction<number>>(actionCreator())
@@ -104,9 +104,9 @@ function expectType<T>(p: T): T {
     { type: 'action' }
   ) as PayloadActionCreator<number>
 
-  const actionCreator2: ActionCreator<
-    PayloadAction<number>
-  > = payloadActionCreator2
+  const actionCreator2: ActionCreator<PayloadAction<
+    number
+  >> = payloadActionCreator2
 }
 
 /* createAction() */
diff --git a/type-tests/files/createAsyncThunk.typetest.ts b/type-tests/files/createAsyncThunk.typetest.ts
new file mode 100644
index 00000000..261f459e
--- /dev/null
+++ b/type-tests/files/createAsyncThunk.typetest.ts
@@ -0,0 +1,183 @@
+import { createAsyncThunk, Dispatch, createReducer, AnyAction } from 'src'
+import { ThunkDispatch } from 'redux-thunk'
+import { unwrapResult, SerializedError } from 'src/createAsyncThunk'
+
+import apiRequest, { AxiosError } from 'axios'
+import { IsAny } from 'src/tsHelpers'
+
+function expectType<T>(t: T) {
+  return t
+}
+const defaultDispatch = (() => {}) as ThunkDispatch<{}, any, AnyAction>
+
+  // basic usage
+;(async function() {
+  const async = createAsyncThunk('test', (id: number) =>
+    Promise.resolve(id * 2)
+  )
+
+  const reducer = createReducer({}, builder =>
+    builder
+      .addCase(async.pending, (_, action) => {
+        expectType<ReturnType<typeof async['pending']>>(action)
+      })
+      .addCase(async.fulfilled, (_, action) => {
+        expectType<ReturnType<typeof async['fulfilled']>>(action)
+        expectType<number>(action.payload)
+      })
+      .addCase(async.rejected, (_, action) => {
+        expectType<ReturnType<typeof async['rejected']>>(action)
+        expectType<Partial<Error> | undefined>(action.error)
+      })
+  )
+
+  const promise = defaultDispatch(async(3))
+  const result = await promise
+
+  if (async.fulfilled.match(result)) {
+    expectType<ReturnType<typeof async['fulfilled']>>(result)
+    // typings:expect-error
+    expectType<ReturnType<typeof async['rejected']>>(result)
+  } else {
+    expectType<ReturnType<typeof async['rejected']>>(result)
+    // typings:expect-error
+    expectType<ReturnType<typeof async['fulfilled']>>(result)
+  }
+
+  promise
+    .then(unwrapResult)
+    .then(result => {
+      expectType<number>(result)
+      // typings:expect-error
+      expectType<Error>(result)
+    })
+    .catch(error => {
+      // catch is always any-typed, nothing we can do here
+    })
+})()
+
+// More complex usage of thunk args
+;(async function() {
+  interface BookModel {
+    id: string
+    title: string
+  }
+
+  type BooksState = BookModel[]
+
+  const fakeBooks: BookModel[] = [
+    { id: 'b', title: 'Second' },
+    { id: 'a', title: 'First' }
+  ]
+
+  const correctDispatch = (() => {}) as ThunkDispatch<
+    BookModel[],
+    { userAPI: Function },
+    AnyAction
+  >
+
+  // Verify that the the first type args to createAsyncThunk line up right
+  const fetchBooksTAC = createAsyncThunk<
+    BookModel[],
+    number,
+    {
+      state: BooksState
+      extra: { userAPI: Function }
+    }
+  >(
+    'books/fetch',
+    async (arg, { getState, dispatch, extra, requestId, signal }) => {
+      const state = getState()
+
+      expectType<number>(arg)
+      expectType<BookModel[]>(state)
+      expectType<{ userAPI: Function }>(extra)
+      return fakeBooks
+    }
+  )
+
+  correctDispatch(fetchBooksTAC(1))
+  // typings:expect-error
+  defaultDispatch(fetchBooksTAC(1))
+})()
+/**
+ * returning a rejected action from the promise creator is possible
+ */
+;(async () => {
+  type ReturnValue = { data: 'success' }
+  type RejectValue = { data: 'error' }
+
+  const fetchBooksTAC = createAsyncThunk<
+    ReturnValue,
+    number,
+    {
+      rejectValue: RejectValue
+    }
+  >('books/fetch', async (arg, { rejectWithValue }) => {
+    return rejectWithValue({ data: 'error' })
+  })
+
+  const returned = await defaultDispatch(fetchBooksTAC(1))
+  if (fetchBooksTAC.rejected.match(returned)) {
+    expectType<undefined | RejectValue>(returned.payload)
+    expectType<RejectValue>(returned.payload!)
+  } else {
+    expectType<ReturnValue>(returned.payload)
+  }
+})()
+
+{
+  interface Item {
+    name: string
+  }
+
+  interface ErrorFromServer {
+    error: string
+  }
+
+  interface CallsResponse {
+    data: Item[]
+  }
+
+  const fetchLiveCallsError = createAsyncThunk<
+    Item[],
+    string,
+    {
+      rejectValue: ErrorFromServer
+    }
+  >('calls/fetchLiveCalls', async (organizationId, { rejectWithValue }) => {
+    try {
+      const result = await apiRequest.get<CallsResponse>(
+        `organizations/${organizationId}/calls/live/iwill404`
+      )
+      return result.data.data
+    } catch (err) {
+      let error: AxiosError<ErrorFromServer> = err // cast for access to AxiosError properties
+      if (!error.response) {
+        // let it be handled as any other unknown error
+        throw err
+      }
+      return rejectWithValue(error.response && error.response.data)
+    }
+  })
+
+  defaultDispatch(fetchLiveCallsError('asd')).then(result => {
+    if (fetchLiveCallsError.fulfilled.match(result)) {
+      //success
+      expectType<ReturnType<typeof fetchLiveCallsError['fulfilled']>>(result)
+      expectType<Item[]>(result.payload)
+    } else {
+      expectType<ReturnType<typeof fetchLiveCallsError['rejected']>>(result)
+      if (result.payload) {
+        // rejected with value
+        expectType<ErrorFromServer>(result.payload)
+      } else {
+        // rejected by throw
+        expectType<undefined>(result.payload)
+        expectType<SerializedError>(result.error)
+        // typings:expect-error
+        expectType<IsAny<typeof result['error'], true, false>>(true)
+      }
+    }
+  })
+}
diff --git a/type-tests/files/createEntityAdapter.typetest.ts b/type-tests/files/createEntityAdapter.typetest.ts
new file mode 100644
index 00000000..c53a29ff
--- /dev/null
+++ b/type-tests/files/createEntityAdapter.typetest.ts
@@ -0,0 +1,113 @@
+import {
+  createSlice,
+  createEntityAdapter,
+  EntityAdapter,
+  ActionCreatorWithPayload,
+  ActionCreatorWithoutPayload
+} from 'src'
+import { EntityStateAdapter, EntityId, Update } from 'src/entities/models'
+
+function expectType<T>(t: T) {
+  return t
+}
+
+function extractReducers<T>(
+  adapter: EntityAdapter<T>
+): Omit<EntityStateAdapter<T>, 'map'> {
+  const {
+    selectId,
+    sortComparer,
+    getInitialState,
+    getSelectors,
+    ...rest
+  } = adapter
+  return rest
+}
+
+/**
+ * should be usable in a slice, with all the "reducer-like" functions
+ */
+{
+  type Entity = {
+    value: string
+  }
+  const adapter = createEntityAdapter<Entity>()
+  const slice = createSlice({
+    name: 'test',
+    initialState: adapter.getInitialState(),
+    reducers: {
+      ...extractReducers(adapter)
+    }
+  })
+
+  expectType<ActionCreatorWithPayload<Entity>>(slice.actions.addOne)
+  expectType<ActionCreatorWithPayload<Entity[]>>(slice.actions.addMany)
+  expectType<ActionCreatorWithPayload<Entity[]>>(slice.actions.setAll)
+  expectType<ActionCreatorWithPayload<EntityId>>(slice.actions.removeOne)
+  expectType<ActionCreatorWithPayload<EntityId[]>>(slice.actions.removeMany)
+  expectType<ActionCreatorWithoutPayload>(slice.actions.removeAll)
+  expectType<ActionCreatorWithPayload<Update<Entity>>>(slice.actions.updateOne)
+  expectType<ActionCreatorWithPayload<Update<Entity>[]>>(
+    slice.actions.updateMany
+  )
+  expectType<ActionCreatorWithPayload<Entity>>(slice.actions.upsertOne)
+  expectType<ActionCreatorWithPayload<Entity[]>>(slice.actions.upsertMany)
+}
+
+/**
+ * should not be able to mix with a different EntityAdapter
+ */
+{
+  type Entity = {
+    value: string
+  }
+  type Entity2 = {
+    value2: string
+  }
+  const adapter = createEntityAdapter<Entity>()
+  const adapter2 = createEntityAdapter<Entity2>()
+  createSlice({
+    name: 'test',
+    initialState: adapter.getInitialState(),
+    reducers: {
+      addOne: adapter.addOne,
+      // typings:expect-error
+      addOne2: adapter2.addOne
+    }
+  })
+}
+
+/**
+ * should be usable in a slice with extra properties
+ */
+{
+  type Entity = {
+    value: string
+  }
+  const adapter = createEntityAdapter<Entity>()
+  createSlice({
+    name: 'test',
+    initialState: adapter.getInitialState({ extraData: 'test' }),
+    reducers: {
+      addOne: adapter.addOne
+    }
+  })
+}
+
+/**
+ * should not be usable in a slice with an unfitting state
+ */
+{
+  type Entity = {
+    value: string
+  }
+  const adapter = createEntityAdapter<Entity>()
+  createSlice({
+    name: 'test',
+    initialState: { somethingElse: '' },
+    reducers: {
+      // typings:expect-error
+      addOne: adapter.addOne
+    }
+  })
+}