From 7fc98bb58738bab084dcd389189fe6128bf250c4 Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Sun, 15 Mar 2020 18:16:04 +0100 Subject: [PATCH 1/5] prevent any-casting of S generic on entityAdapter reducer-like functions --- src/entities/models.ts | 71 +++++++---- .../files/createEntityAdapter.typetest.ts | 114 ++++++++++++++++++ 2 files changed, 164 insertions(+), 21 deletions(-) create mode 100644 type-tests/files/createEntityAdapter.typetest.ts diff --git a/src/entities/models.ts b/src/entities/models.ts index 9f7b217d4f..cdfe39f695 100644 --- a/src/entities/models.ts +++ b/src/entities/models.ts @@ -1,4 +1,5 @@ import { PayloadAction } from '../createAction' +import { IsAny } from '../tsHelpers' /** * @alpha @@ -52,50 +53,78 @@ export interface EntityDefinition { sortComparer: false | Comparer } +type PreventAny = IsAny, S> + export interface EntityStateAdapter { - addOne>(state: S, entity: T): S - addOne>(state: S, action: PayloadAction): S + addOne>(state: PreventAny, entity: T): S + addOne>( + state: PreventAny, + action: PayloadAction + ): S - addMany>(state: S, entities: T[]): S - addMany>(state: S, entities: PayloadAction): S + addMany>(state: PreventAny, entities: T[]): S + addMany>( + state: PreventAny, + entities: PayloadAction + ): S - setAll>(state: S, entities: T[]): S - setAll>(state: S, entities: PayloadAction): S + setAll>(state: PreventAny, entities: T[]): S + setAll>( + state: PreventAny, + entities: PayloadAction + ): S - removeOne>(state: S, key: EntityId): S - removeOne>(state: S, key: PayloadAction): S + removeOne>(state: PreventAny, key: EntityId): S + removeOne>( + state: PreventAny, + key: PayloadAction + ): S - removeMany>(state: S, keys: EntityId[]): S removeMany>( - state: S, + state: PreventAny, + keys: EntityId[] + ): S + removeMany>( + state: PreventAny, keys: PayloadAction ): S - removeAll>(state: S): S + removeAll>(state: PreventAny): S - updateOne>(state: S, update: Update): S updateOne>( - state: S, + state: PreventAny, + update: Update + ): S + updateOne>( + state: PreventAny, update: PayloadAction> ): S - updateMany>(state: S, updates: Update[]): S updateMany>( - state: S, + state: PreventAny, + updates: Update[] + ): S + updateMany>( + state: PreventAny, updates: PayloadAction[]> ): S - upsertOne>(state: S, entity: T): S - upsertOne>(state: S, entity: PayloadAction): S + upsertOne>(state: PreventAny, entity: T): S + upsertOne>( + state: PreventAny, + entity: PayloadAction + ): S - upsertMany>(state: S, entities: T[]): S upsertMany>( - state: S, + state: PreventAny, + entities: T[] + ): S + upsertMany>( + state: PreventAny, entities: PayloadAction ): S - map>(state: S, map: EntityMap): S - map>(state: S, map: PayloadAction>): S + map>(state: PreventAny, map: EntityMap): S } export interface EntitySelectors { diff --git a/type-tests/files/createEntityAdapter.typetest.ts b/type-tests/files/createEntityAdapter.typetest.ts new file mode 100644 index 0000000000..9899041607 --- /dev/null +++ b/type-tests/files/createEntityAdapter.typetest.ts @@ -0,0 +1,114 @@ +import { + createSlice, + createEntityAdapter, + EntityAdapter, + ActionCreatorWithPayload, + ActionCreatorWithoutPayload +} from 'src' +import { EntityStateAdapter, EntityId, Update } from 'src/entities/models' + +function expectType(t: T) { + return t +} + +function extractReducers( + adapter: EntityAdapter +): Omit, 'map'> { + const { + selectId, + sortComparer, + getInitialState, + getSelectors, + map, + ...rest + } = adapter + return rest +} + +/** + * should be usable in a slice, with all the "reducer-like" functions + */ +{ + type Entity = { + value: string + } + const adapter = createEntityAdapter() + const slice = createSlice({ + name: 'test', + initialState: adapter.getInitialState(), + reducers: { + ...extractReducers(adapter) + } + }) + + expectType>(slice.actions.addOne) + expectType>(slice.actions.addMany) + expectType>(slice.actions.setAll) + expectType>(slice.actions.removeOne) + expectType>(slice.actions.removeMany) + expectType(slice.actions.removeAll) + expectType>>(slice.actions.updateOne) + expectType[]>>( + slice.actions.updateMany + ) + expectType>(slice.actions.upsertOne) + expectType>(slice.actions.upsertMany) +} + +/** + * should not be able to mix with a different EntityAdapter + */ +{ + type Entity = { + value: string + } + type Entity2 = { + value2: string + } + const adapter = createEntityAdapter() + const adapter2 = createEntityAdapter() + 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() + 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() + createSlice({ + name: 'test', + initialState: { somethingElse: '' }, + reducers: { + // typings:expect-error + addOne: adapter.addOne + } + }) +} From 9bb035e37a7d3084da17911876d72462976c105d Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Sun, 15 Mar 2020 18:45:03 +0100 Subject: [PATCH 2/5] remove `map` from entityAdapter --- src/entities/models.ts | 9 +---- src/entities/sorted_state_adapter.test.ts | 34 ------------------- src/entities/sorted_state_adapter.ts | 22 ++---------- src/entities/unsorted_state_adapter.test.ts | 34 ------------------- src/entities/unsorted_state_adapter.ts | 22 ++---------- src/index.ts | 1 - .../files/createEntityAdapter.typetest.ts | 1 - 7 files changed, 5 insertions(+), 118 deletions(-) diff --git a/src/entities/models.ts b/src/entities/models.ts index cdfe39f695..30996a57cb 100644 --- a/src/entities/models.ts +++ b/src/entities/models.ts @@ -35,11 +35,6 @@ export interface Dictionary extends DictionaryNum { */ export type Update = { id: EntityId; changes: Partial } -/** - * @alpha - */ -export type EntityMap = (entity: T) => T - /** * @alpha */ @@ -53,7 +48,7 @@ export interface EntityDefinition { sortComparer: false | Comparer } -type PreventAny = IsAny, S> +export type PreventAny = IsAny, S> export interface EntityStateAdapter { addOne>(state: PreventAny, entity: T): S @@ -123,8 +118,6 @@ export interface EntityStateAdapter { state: PreventAny, entities: PayloadAction ): S - - map>(state: PreventAny, map: EntityMap): S } export interface EntitySelectors { diff --git a/src/entities/sorted_state_adapter.test.ts b/src/entities/sorted_state_adapter.test.ts index 13419d78e5..8b45a78868 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/sorted_state_adapter.ts b/src/entities/sorted_state_adapter.ts index 79e6e03f3b..93c6e5e7a4 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( } } - function mapMutably(updatesOrMap: EntityMap, state: R): void { - const updates: Update[] = state.ids.reduce( - (changes: Update[], 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( 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.test.ts b/src/entities/unsorted_state_adapter.test.ts index 5b83f1455e..c444833738 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/src/entities/unsorted_state_adapter.ts b/src/entities/unsorted_state_adapter.ts index 5791abead7..7386d9a820 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( } } - function removeAll(state: S): S { + function removeAll(state: R): any { return Object.assign({}, state, { ids: [], entities: {} @@ -120,22 +119,6 @@ export function createUnsortedStateAdapter( } } - function mapMutably(map: EntityMap, state: R): void { - const changes: Update[] = state.ids.reduce( - (changes: Update[], 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( 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 aa484c2f2d..c61b909506 100644 --- a/src/index.ts +++ b/src/index.ts @@ -75,7 +75,6 @@ export { EntityState, EntityAdapter, Update, - EntityMap, IdSelector, Comparer } from './entities/models' diff --git a/type-tests/files/createEntityAdapter.typetest.ts b/type-tests/files/createEntityAdapter.typetest.ts index 9899041607..c53a29ff2e 100644 --- a/type-tests/files/createEntityAdapter.typetest.ts +++ b/type-tests/files/createEntityAdapter.typetest.ts @@ -19,7 +19,6 @@ function extractReducers( sortComparer, getInitialState, getSelectors, - map, ...rest } = adapter return rest From 7c049fd950dc7ed8c391d3d4c6cd72a528982788 Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Sun, 15 Mar 2020 18:49:12 +0100 Subject: [PATCH 3/5] remove references to `map` from the docs --- docs/api/createEntityAdapter.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/api/createEntityAdapter.md b/docs/api/createEntityAdapter.md index d3ffda2e07..373b8ef28c 100644 --- a/docs/api/createEntityAdapter.md +++ b/docs/api/createEntityAdapter.md @@ -118,8 +118,6 @@ export interface Dictionary extends DictionaryNum { export type Update = { id: EntityId; changes: Partial } -export type EntityMap = (entity: T) => T - export interface EntityState { ids: EntityId[] entities: Dictionary @@ -171,9 +169,6 @@ export interface EntityStateAdapter { state: S, entities: PayloadAction ): S - - map>(state: S, map: EntityMap): S - map>(state: S, map: PayloadAction>): S } export interface EntitySelectors { @@ -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: From e05698b59b8ee8c14fdb3694091134e8304b7e5d Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Sun, 15 Mar 2020 18:49:20 +0100 Subject: [PATCH 4/5] update API report --- etc/redux-toolkit.api.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md index d8e0eedef7..870e272cbe 100644 --- a/etc/redux-toolkit.api.md +++ b/etc/redux-toolkit.api.md @@ -185,9 +185,6 @@ export interface EntityAdapter extends EntityStateAdapter { sortComparer: false | Comparer; } -// @alpha (undocumented) -export type EntityMap = (entity: T) => T; - // @alpha (undocumented) export interface EntityState { // (undocumented) From 4af88b92c8f78ec92643af5d74ef01127cdbc5f4 Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Sun, 15 Mar 2020 18:55:24 +0100 Subject: [PATCH 5/5] remove export --- src/entities/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/entities/index.ts b/src/entities/index.ts index eded42c904..e6fe1d87db 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -4,7 +4,6 @@ export { EntityState, EntityAdapter, Update, - EntityMap, IdSelector, Comparer } from './models'