Repository: reduxjs/redux-toolkit
Pull request #436 (2020-03-15 17:27:06)

prevent any-casting of S generic on entityAdapter reducer-like f…
This would prevent what we were seeing in #434. I'm preventing these methods from having their `state` parameter from falling back to `any`.
Note that I do *not* do this for their return type though, because that would make the "should be usable in a slice with extra properties" test I added fail. 
I think like this it's a good compromise of a fallback.

Additionally, I've added type-tests.

Also, I've removed the entityAdapter.map method, as that cannot be used as a reducer (action would be non-serializable), is easily re-implemented in userland and the name `map` implies a potentially completely different return type of a non-mutating copy, which is not really the case here.

markerikson: Can you update the docs page as well?
phryneas: Sure.
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/436/builds/14974) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 4af88b92c8f78ec92643af5d74ef01127cdbc5f4:

|Sandbox| Source |
|--|--|
|[hungry-brahmagupta-07u10](https://codesandbox.io/s/hungry-brahmagupta-07u10)| Configuration |
|[goofy-dew-vlr46](https://codesandbox.io/s/goofy-dew-vlr46)| Configuration |
|[gifted-rain-lqmc8](https://codesandbox.io/s/gifted-rain-lqmc8)| Configuration |


Patch:
diff --git a/docs/api/createEntityAdapter.md b/docs/api/createEntityAdapter.md
index d3ffda2e..373b8ef2 100644
--- a/docs/api/createEntityAdapter.md
+++ b/docs/api/createEntityAdapter.md
@@ -118,8 +118,6 @@ export interface Dictionary<T> extends DictionaryNum<T> {
 
 export type Update<T> = { id: EntityId; changes: Partial<T> }
 
-export type EntityMap<T> = (entity: T) => T
-
 export interface EntityState<T> {
   ids: EntityId[]
   entities: Dictionary<T>
@@ -171,9 +169,6 @@ export interface EntityStateAdapter<T> {
     state: S,
     entities: PayloadAction<T[]>
   ): S
-
-  map<S extends EntityState<T>>(state: S, map: EntityMap<T>): S
-  map<S extends EntityState<T>>(state: S, map: PayloadAction<EntityMap<T>>): S
 }
 
 export interface EntitySelectors<T, V> {
@@ -208,7 +203,6 @@ The primary content of an entity adapter is a set of generated reducer functions
 - `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.
-- `map`: accepts a callback function that will be run against each existing entity, and may return a change description object. Afterwards, all changes will be merged into the corresponding existing entities.
 
 Each method has a signature that looks like:
 
diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md
index d8e0eede..870e272c 100644
--- a/etc/redux-toolkit.api.md
+++ b/etc/redux-toolkit.api.md
@@ -185,9 +185,6 @@ export interface EntityAdapter<T> extends EntityStateAdapter<T> {
     sortComparer: false | Comparer<T>;
 }
 
-// @alpha (undocumented)
-export type EntityMap<T> = (entity: T) => T;
-
 // @alpha (undocumented)
 export interface EntityState<T> {
     // (undocumented)
diff --git a/src/entities/index.ts b/src/entities/index.ts
index eded42c9..e6fe1d87 100644
--- a/src/entities/index.ts
+++ b/src/entities/index.ts
@@ -4,7 +4,6 @@ export {
   EntityState,
   EntityAdapter,
   Update,
-  EntityMap,
   IdSelector,
   Comparer
 } from './models'
diff --git a/src/entities/models.ts b/src/entities/models.ts
index 9f7b217d..30996a57 100644
--- a/src/entities/models.ts
+++ b/src/entities/models.ts
@@ -1,4 +1,5 @@
 import { PayloadAction } from '../createAction'
+import { IsAny } from '../tsHelpers'
 
 /**
  * @alpha
@@ -34,11 +35,6 @@ export interface Dictionary<T> extends DictionaryNum<T> {
  */
 export type Update<T> = { id: EntityId; changes: Partial<T> }
 
-/**
- * @alpha
- */
-export type EntityMap<T> = (entity: T) => T
-
 /**
  * @alpha
  */
@@ -52,50 +48,76 @@ export interface EntityDefinition<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: S, entity: T): S
-  addOne<S extends EntityState<T>>(state: S, action: PayloadAction<T>): S
+  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: S, entities: T[]): S
-  addMany<S extends EntityState<T>>(state: S, entities: 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: S, entities: T[]): S
-  setAll<S extends EntityState<T>>(state: S, 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: S, key: EntityId): S
-  removeOne<S extends EntityState<T>>(state: S, key: PayloadAction<EntityId>): 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: S, keys: EntityId[]): S
   removeMany<S extends EntityState<T>>(
-    state: S,
+    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: S): S
+  removeAll<S extends EntityState<T>>(state: PreventAny<S, T>): S
 
-  updateOne<S extends EntityState<T>>(state: S, update: Update<T>): S
   updateOne<S extends EntityState<T>>(
-    state: S,
+    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: S, updates: Update<T>[]): S
   updateMany<S extends EntityState<T>>(
-    state: S,
+    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: S, entity: T): S
-  upsertOne<S extends EntityState<T>>(state: S, entity: PayloadAction<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: S, entities: T[]): S
   upsertMany<S extends EntityState<T>>(
-    state: S,
+    state: PreventAny<S, T>,
+    entities: T[]
+  ): S
+  upsertMany<S extends EntityState<T>>(
+    state: PreventAny<S, T>,
     entities: PayloadAction<T[]>
   ): S
-
-  map<S extends EntityState<T>>(state: S, map: EntityMap<T>): S
-  map<S extends EntityState<T>>(state: S, map: PayloadAction<EntityMap<T>>): S
 }
 
 export interface EntitySelectors<T, V> {
diff --git a/src/entities/sorted_state_adapter.ts b/src/entities/sorted_state_adapter.ts
index 79e6e03f..93c6e5e7 100644
--- a/src/entities/sorted_state_adapter.ts
+++ b/src/entities/sorted_state_adapter.ts
@@ -3,9 +3,7 @@ import {
   IdSelector,
   Comparer,
   EntityStateAdapter,
-  Update,
-  EntityMap,
-  EntityId
+  Update
 } from './models'
 import { createStateOperator } from './state_adapter'
 import { createUnsortedStateAdapter } from './unsorted_state_adapter'
@@ -72,21 +70,6 @@ export function createSortedStateAdapter<T>(
     }
   }
 
-  function mapMutably(updatesOrMap: EntityMap<T>, state: R): void {
-    const updates: Update<T>[] = state.ids.reduce(
-      (changes: Update<T>[], id: EntityId) => {
-        const change = updatesOrMap(state.entities[id]!)
-        if (change !== state.entities[id]) {
-          changes.push({ id, changes: change })
-        }
-        return changes
-      },
-      []
-    )
-
-    updateManyMutably(updates, state)
-  }
-
   function upsertOneMutably(entity: T, state: R): void {
     return upsertManyMutably([entity], state)
   }
@@ -151,7 +134,6 @@ export function createSortedStateAdapter<T>(
     setAll: createStateOperator(setAllMutably),
     addMany: createStateOperator(addManyMutably),
     updateMany: createStateOperator(updateManyMutably),
-    upsertMany: createStateOperator(upsertManyMutably),
-    map: createStateOperator(mapMutably)
+    upsertMany: createStateOperator(upsertManyMutably)
   }
 }
diff --git a/src/entities/unsorted_state_adapter.ts b/src/entities/unsorted_state_adapter.ts
index 5791abea..7386d9a8 100644
--- a/src/entities/unsorted_state_adapter.ts
+++ b/src/entities/unsorted_state_adapter.ts
@@ -3,7 +3,6 @@ import {
   EntityStateAdapter,
   IdSelector,
   Update,
-  EntityMap,
   EntityId
 } from './models'
 import { createStateOperator } from './state_adapter'
@@ -57,7 +56,7 @@ export function createUnsortedStateAdapter<T>(
     }
   }
 
-  function removeAll<S extends R>(state: S): S {
+  function removeAll(state: R): any {
     return Object.assign({}, state, {
       ids: [],
       entities: {}
@@ -120,22 +119,6 @@ export function createUnsortedStateAdapter<T>(
     }
   }
 
-  function mapMutably(map: EntityMap<T>, state: R): void {
-    const changes: Update<T>[] = state.ids.reduce(
-      (changes: Update<T>[], id: EntityId) => {
-        const change = map(state.entities[id]!)
-        if (change !== state.entities[id]) {
-          changes.push({ id, changes: change })
-        }
-        return changes
-      },
-      []
-    )
-    const updates = changes.filter(({ id }) => id in state.entities)
-
-    return updateManyMutably(updates, state)
-  }
-
   function upsertOneMutably(entity: T, state: R): void {
     return upsertManyMutably([entity], state)
   }
@@ -167,7 +150,6 @@ export function createUnsortedStateAdapter<T>(
     upsertOne: createStateOperator(upsertOneMutably),
     upsertMany: createStateOperator(upsertManyMutably),
     removeOne: createStateOperator(removeOneMutably),
-    removeMany: createStateOperator(removeManyMutably),
-    map: createStateOperator(mapMutably)
+    removeMany: createStateOperator(removeManyMutably)
   }
 }
diff --git a/src/index.ts b/src/index.ts
index aa484c2f..c61b9095 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -75,7 +75,6 @@ export {
   EntityState,
   EntityAdapter,
   Update,
-  EntityMap,
   IdSelector,
   Comparer
 } from './entities/models'


Test patch:
diff --git a/src/entities/sorted_state_adapter.test.ts b/src/entities/sorted_state_adapter.test.ts
index 13419d78..8b45a788 100644
--- a/src/entities/sorted_state_adapter.test.ts
+++ b/src/entities/sorted_state_adapter.test.ts
@@ -331,40 +331,6 @@ describe('Sorted State Adapter', () => {
     })
   })
 
-  it('should let you map over entities in the state', () => {
-    const firstChange = { ...TheGreatGatsby, title: 'First change' }
-    const secondChange = { ...AClockworkOrange, title: 'Second change' }
-
-    const withMany = adapter.setAll(state, [
-      TheGreatGatsby,
-      AClockworkOrange,
-      AnimalFarm
-    ])
-
-    const withUpdates = adapter.map(withMany, book =>
-      book.title === TheGreatGatsby.title
-        ? firstChange
-        : book.title === AClockworkOrange.title
-        ? secondChange
-        : book
-    )
-
-    expect(withUpdates).toEqual({
-      ids: [AnimalFarm.id, TheGreatGatsby.id, AClockworkOrange.id],
-      entities: {
-        [AnimalFarm.id]: AnimalFarm,
-        [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({
diff --git a/src/entities/unsorted_state_adapter.test.ts b/src/entities/unsorted_state_adapter.test.ts
index 5b83f145..c4448337 100644
--- a/src/entities/unsorted_state_adapter.test.ts
+++ b/src/entities/unsorted_state_adapter.test.ts
@@ -250,40 +250,6 @@ describe('Unsorted State Adapter', () => {
     expect(entities.c).toBeTruthy()
   })
 
-  it('should let you map over entities in the state', () => {
-    const firstChange = { ...TheGreatGatsby, title: 'First change' }
-    const secondChange = { ...AClockworkOrange, title: 'Second change' }
-
-    const withMany = adapter.setAll(state, [
-      TheGreatGatsby,
-      AClockworkOrange,
-      AnimalFarm
-    ])
-
-    const withUpdates = adapter.map(withMany, book =>
-      book.title === TheGreatGatsby.title
-        ? firstChange
-        : book.title === AClockworkOrange.title
-        ? secondChange
-        : book
-    )
-
-    expect(withUpdates).toEqual({
-      ids: [TheGreatGatsby.id, AClockworkOrange.id, AnimalFarm.id],
-      entities: {
-        [TheGreatGatsby.id]: {
-          ...TheGreatGatsby,
-          ...firstChange
-        },
-        [AClockworkOrange.id]: {
-          ...AClockworkOrange,
-          ...secondChange
-        },
-        [AnimalFarm.id]: AnimalFarm
-      }
-    })
-  })
-
   it('should let you add one entity to the state with upsert()', () => {
     const withOneEntity = adapter.upsertOne(state, TheGreatGatsby)
     expect(withOneEntity).toEqual({
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
+    }
+  })
+}