Skip to content

Commit 96b3084

Browse files
committed
add named selectors
1 parent ca7b1da commit 96b3084

File tree

4 files changed

+185
-63
lines changed

4 files changed

+185
-63
lines changed

packages/toolkit/src/entities/models.ts

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { Draft } from 'immer'
33
import type { PayloadAction } from '../createAction'
44
import type { GetSelectorsOptions } from './state_selectors'
55
import type { CastAny, Id as Compute } from '../tsHelpers'
6+
import type { CaseReducerDefinition } from '../createSlice'
7+
import type { CaseReducer } from '../createReducer'
68

79
/**
810
* @public
@@ -158,12 +160,49 @@ export interface EntityStateAdapter<T, Id extends EntityId> {
158160
/**
159161
* @public
160162
*/
161-
export interface EntitySelectors<T, V, Id extends EntityId> {
162-
selectIds: (state: V) => Id[]
163-
selectEntities: (state: V) => Record<Id, T>
164-
selectAll: (state: V) => T[]
165-
selectTotal: (state: V) => number
166-
selectById: (state: V, id: Id) => Compute<UncheckedIndexedAccess<T>>
163+
export type EntitySelectors<
164+
T,
165+
V,
166+
Id extends EntityId,
167+
Single extends string = '',
168+
Plural extends string = DefaultPlural<Single>,
169+
> = {
170+
[K in `select${Capitalize<Single>}Ids`]: (state: V) => Id[]
171+
} & {
172+
[K in `select${Capitalize<Single>}Entities`]: (state: V) => Record<Id, T>
173+
} & {
174+
[K in `selectAll${Capitalize<Plural>}`]: (state: V) => T[]
175+
} & {
176+
[K in `selectTotal${Capitalize<Plural>}`]: (state: V) => number
177+
} & {
178+
[K in `select${Capitalize<Single>}ById`]: (
179+
state: V,
180+
id: Id,
181+
) => Compute<UncheckedIndexedAccess<T>>
182+
}
183+
184+
export type DefaultPlural<Single extends string> = Single extends ''
185+
? ''
186+
: `${Single}s`
187+
188+
export type EntityReducers<
189+
T,
190+
Id extends EntityId,
191+
State = EntityState<T, Id>,
192+
Single extends string = '',
193+
Plural extends string = DefaultPlural<Single>,
194+
> = {
195+
[K in keyof EntityStateAdapter<
196+
T,
197+
Id
198+
> as `${K}${Capitalize<K extends `${string}One` ? Single : Plural>}`]: EntityStateAdapter<
199+
T,
200+
Id
201+
>[K] extends (state: any) => any
202+
? CaseReducerDefinition<State, PayloadAction>
203+
: EntityStateAdapter<T, Id>[K] extends CaseReducer<any, infer A>
204+
? CaseReducerDefinition<State, A>
205+
: never
167206
}
168207

169208
/**
@@ -175,12 +214,19 @@ export interface EntityAdapter<T, Id extends EntityId>
175214
sortComparer: false | Comparer<T>
176215
getInitialState(): EntityState<T, Id>
177216
getInitialState<S extends object>(state: S): EntityState<T, Id> & S
178-
getSelectors(
217+
getSelectors<
218+
Single extends string = '',
219+
Plural extends string = DefaultPlural<Single>,
220+
>(
179221
selectState?: undefined,
180-
options?: GetSelectorsOptions,
181-
): EntitySelectors<T, EntityState<T, Id>, Id>
182-
getSelectors<V>(
222+
options?: GetSelectorsOptions<Single, Plural>,
223+
): EntitySelectors<T, EntityState<T, Id>, Id, Single, Plural>
224+
getSelectors<
225+
V,
226+
Single extends string = '',
227+
Plural extends string = DefaultPlural<Single>,
228+
>(
183229
selectState: (state: V) => EntityState<T, Id>,
184-
options?: GetSelectorsOptions,
185-
): EntitySelectors<T, V, Id>
230+
options?: GetSelectorsOptions<Single, Plural>,
231+
): EntitySelectors<T, V, Id, Single, Plural>
186232
}

packages/toolkit/src/entities/slice_creator.ts

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,22 @@
11
import type {
2-
CaseReducer,
32
CreatorCaseReducers,
43
ReducerCreator,
54
ReducerCreatorEntry,
65
} from '@reduxjs/toolkit'
7-
import type { PayloadAction } from '../createAction'
8-
import { reducerCreator, type CaseReducerDefinition } from '../createSlice'
6+
import { reducerCreator } from '../createSlice'
97
import type { WithRequiredProp } from '../tsHelpers'
108
import type {
119
Update,
1210
EntityAdapter,
1311
EntityId,
1412
EntityState,
15-
EntityStateAdapter,
13+
DefaultPlural,
14+
EntityReducers,
1615
} from './models'
1716
import { capitalize } from './utils'
1817

1918
export const entityMethodsCreatorType = /*@__PURE__*/ Symbol()
2019

21-
type DefaultPlural<Single extends string> = Single extends ''
22-
? ''
23-
: `${Single}s`
24-
25-
type EntityReducers<
26-
T,
27-
Id extends EntityId,
28-
State = EntityState<T, Id>,
29-
Single extends string = '',
30-
Plural extends string = DefaultPlural<Single>,
31-
> = {
32-
[K in keyof EntityStateAdapter<
33-
T,
34-
Id
35-
> as `${K}${Capitalize<K extends `${string}One` ? Single : Plural>}`]: EntityStateAdapter<
36-
T,
37-
Id
38-
>[K] extends (state: any) => any
39-
? CaseReducerDefinition<State, PayloadAction>
40-
: EntityStateAdapter<T, Id>[K] extends CaseReducer<any, infer A>
41-
? CaseReducerDefinition<State, A>
42-
: never
43-
}
44-
4520
export interface EntityMethodsCreatorConfig<
4621
T,
4722
Id extends EntityId,

packages/toolkit/src/entities/state_selectors.ts

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,56 @@
11
import type { CreateSelectorFunction, Selector, createSelector } from 'reselect'
22
import { createDraftSafeSelector } from '../createDraftSafeSelector'
3-
import type { EntityState, EntitySelectors, EntityId } from './models'
3+
import type {
4+
EntityState,
5+
EntitySelectors,
6+
EntityId,
7+
DefaultPlural,
8+
} from './models'
9+
import { capitalize } from './utils'
410

511
type AnyFunction = (...args: any) => any
612
type AnyCreateSelectorFunction = CreateSelectorFunction<
713
<F extends AnyFunction>(f: F) => F,
814
<F extends AnyFunction>(f: F) => F
915
>
1016

11-
export interface GetSelectorsOptions {
17+
export interface GetSelectorsOptions<
18+
Single extends string = '',
19+
Plural extends string = DefaultPlural<''>,
20+
> {
1221
createSelector?: AnyCreateSelectorFunction
22+
name?: Single
23+
pluralName?: Plural
1324
}
1425

1526
export function createSelectorsFactory<T, Id extends EntityId>() {
16-
function getSelectors(
27+
function getSelectors<
28+
Single extends string = '',
29+
Plural extends string = DefaultPlural<Single>,
30+
>(
1731
selectState?: undefined,
18-
options?: GetSelectorsOptions,
19-
): EntitySelectors<T, EntityState<T, Id>, Id>
20-
function getSelectors<V>(
32+
options?: GetSelectorsOptions<Single, Plural>,
33+
): EntitySelectors<T, EntityState<T, Id>, Id, Single, Plural>
34+
function getSelectors<
35+
V,
36+
Single extends string = '',
37+
Plural extends string = DefaultPlural<Single>,
38+
>(
2139
selectState: (state: V) => EntityState<T, Id>,
22-
options?: GetSelectorsOptions,
23-
): EntitySelectors<T, V, Id>
24-
function getSelectors<V>(
40+
options?: GetSelectorsOptions<Single, Plural>,
41+
): EntitySelectors<T, V, Id, Single, Plural>
42+
function getSelectors<
43+
V,
44+
Single extends string = '',
45+
Plural extends string = DefaultPlural<Single>,
46+
>(
2547
selectState?: (state: V) => EntityState<T, Id>,
26-
options: GetSelectorsOptions = {},
27-
): EntitySelectors<T, any, Id> {
48+
options: GetSelectorsOptions<Single, Plural> = {},
49+
): EntitySelectors<T, any, Id, Single, Plural> {
2850
const {
2951
createSelector = createDraftSafeSelector as AnyCreateSelectorFunction,
52+
name = '',
53+
pluralName = name && `${name}s`,
3054
} = options
3155

3256
const selectIds = (state: EntityState<T, Id>) => state.ids
@@ -45,32 +69,54 @@ export function createSelectorsFactory<T, Id extends EntityId>() {
4569

4670
const selectTotal = createSelector(selectIds, (ids) => ids.length)
4771

72+
// template literal computed keys don't keep their type if there's an unresolved generic
73+
// so we cast to some intermediate type to at least check we're using the right variables in the right places
74+
75+
const single = name as 's'
76+
const plural = pluralName as 'p'
77+
4878
if (!selectState) {
49-
return {
50-
selectIds,
51-
selectEntities,
52-
selectAll,
53-
selectTotal,
54-
selectById: createSelector(selectEntities, selectId, selectById),
79+
const selectors: EntitySelectors<T, any, Id, 's', 'p'> = {
80+
[`select${capitalize(single)}Ids` as const]: selectIds,
81+
[`select${capitalize(single)}Entities` as const]: selectEntities,
82+
[`selectAll${capitalize(plural)}` as const]: selectAll,
83+
[`selectTotal${capitalize(plural)}` as const]: selectTotal,
84+
[`select${capitalize(single)}ById` as const]: createSelector(
85+
selectEntities,
86+
selectId,
87+
selectById,
88+
),
5589
}
90+
return selectors as any
5691
}
5792

5893
const selectGlobalizedEntities = createSelector(
5994
selectState as Selector<V, EntityState<T, Id>>,
6095
selectEntities,
6196
)
6297

63-
return {
64-
selectIds: createSelector(selectState, selectIds),
65-
selectEntities: selectGlobalizedEntities,
66-
selectAll: createSelector(selectState, selectAll),
67-
selectTotal: createSelector(selectState, selectTotal),
68-
selectById: createSelector(
98+
const selectors: EntitySelectors<T, any, Id, 's', 'p'> = {
99+
[`select${capitalize(single)}Ids` as const]: createSelector(
100+
selectState,
101+
selectIds,
102+
),
103+
[`select${capitalize(single)}Entities` as const]:
104+
selectGlobalizedEntities,
105+
[`selectAll${capitalize(plural)}` as const]: createSelector(
106+
selectState,
107+
selectAll,
108+
),
109+
[`selectTotal${capitalize(plural)}` as const]: createSelector(
110+
selectState,
111+
selectTotal,
112+
),
113+
[`select${capitalize(single)}ById` as const]: createSelector(
69114
selectGlobalizedEntities,
70115
selectId,
71116
selectById,
72117
),
73118
}
119+
return selectors as any
74120
}
75121

76122
return { getSelectors }

packages/toolkit/src/entities/tests/state_selectors.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,61 @@ describe('Entity State Selectors', () => {
147147
memoizeSpy.mockClear()
148148
})
149149
})
150+
describe('named selectors', () => {
151+
interface State {
152+
books: EntityState<BookModel, string>
153+
}
154+
155+
let adapter: EntityAdapter<BookModel, string>
156+
let state: State
157+
158+
beforeEach(() => {
159+
adapter = createEntityAdapter({
160+
selectId: (book: BookModel) => book.id,
161+
})
162+
163+
state = {
164+
books: adapter.setAll(adapter.getInitialState(), [
165+
AClockworkOrange,
166+
AnimalFarm,
167+
TheGreatGatsby,
168+
]),
169+
}
170+
})
171+
it('should use the provided name and pluralName', () => {
172+
const selectors = adapter.getSelectors(undefined, {
173+
name: 'book',
174+
})
175+
176+
expect(selectors.selectAllBooks).toBeTypeOf('function')
177+
expect(selectors.selectTotalBooks).toBeTypeOf('function')
178+
expect(selectors.selectBookById).toBeTypeOf('function')
179+
180+
expect(selectors.selectAllBooks(state.books)).toEqual([
181+
AClockworkOrange,
182+
AnimalFarm,
183+
TheGreatGatsby,
184+
])
185+
expect(selectors.selectTotalBooks(state.books)).toEqual(3)
186+
})
187+
it('should use the plural of the provided name', () => {
188+
const selectors = adapter.getSelectors((state: State) => state.books, {
189+
name: 'book',
190+
pluralName: 'bookies',
191+
})
192+
193+
expect(selectors.selectAllBookies).toBeTypeOf('function')
194+
expect(selectors.selectTotalBookies).toBeTypeOf('function')
195+
expect(selectors.selectBookById).toBeTypeOf('function')
196+
197+
expect(selectors.selectAllBookies(state)).toEqual([
198+
AClockworkOrange,
199+
AnimalFarm,
200+
TheGreatGatsby,
201+
])
202+
expect(selectors.selectTotalBookies(state)).toEqual(3)
203+
})
204+
})
150205
})
151206

152207
function expectType<T>(t: T) {

0 commit comments

Comments
 (0)