Skip to content

Commit 7fc98bb

Browse files
committed
prevent any-casting of S generic on entityAdapter reducer-like functions
1 parent 980dcbe commit 7fc98bb

File tree

2 files changed

+164
-21
lines changed

2 files changed

+164
-21
lines changed

src/entities/models.ts

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { PayloadAction } from '../createAction'
2+
import { IsAny } from '../tsHelpers'
23

34
/**
45
* @alpha
@@ -52,50 +53,78 @@ export interface EntityDefinition<T> {
5253
sortComparer: false | Comparer<T>
5354
}
5455

56+
type PreventAny<S, T> = IsAny<S, EntityState<T>, S>
57+
5558
export interface EntityStateAdapter<T> {
56-
addOne<S extends EntityState<T>>(state: S, entity: T): S
57-
addOne<S extends EntityState<T>>(state: S, action: PayloadAction<T>): S
59+
addOne<S extends EntityState<T>>(state: PreventAny<S, T>, entity: T): S
60+
addOne<S extends EntityState<T>>(
61+
state: PreventAny<S, T>,
62+
action: PayloadAction<T>
63+
): S
5864

59-
addMany<S extends EntityState<T>>(state: S, entities: T[]): S
60-
addMany<S extends EntityState<T>>(state: S, entities: PayloadAction<T[]>): S
65+
addMany<S extends EntityState<T>>(state: PreventAny<S, T>, entities: T[]): S
66+
addMany<S extends EntityState<T>>(
67+
state: PreventAny<S, T>,
68+
entities: PayloadAction<T[]>
69+
): S
6170

62-
setAll<S extends EntityState<T>>(state: S, entities: T[]): S
63-
setAll<S extends EntityState<T>>(state: S, entities: PayloadAction<T[]>): S
71+
setAll<S extends EntityState<T>>(state: PreventAny<S, T>, entities: T[]): S
72+
setAll<S extends EntityState<T>>(
73+
state: PreventAny<S, T>,
74+
entities: PayloadAction<T[]>
75+
): S
6476

65-
removeOne<S extends EntityState<T>>(state: S, key: EntityId): S
66-
removeOne<S extends EntityState<T>>(state: S, key: PayloadAction<EntityId>): S
77+
removeOne<S extends EntityState<T>>(state: PreventAny<S, T>, key: EntityId): S
78+
removeOne<S extends EntityState<T>>(
79+
state: PreventAny<S, T>,
80+
key: PayloadAction<EntityId>
81+
): S
6782

68-
removeMany<S extends EntityState<T>>(state: S, keys: EntityId[]): S
6983
removeMany<S extends EntityState<T>>(
70-
state: S,
84+
state: PreventAny<S, T>,
85+
keys: EntityId[]
86+
): S
87+
removeMany<S extends EntityState<T>>(
88+
state: PreventAny<S, T>,
7189
keys: PayloadAction<EntityId[]>
7290
): S
7391

74-
removeAll<S extends EntityState<T>>(state: S): S
92+
removeAll<S extends EntityState<T>>(state: PreventAny<S, T>): S
7593

76-
updateOne<S extends EntityState<T>>(state: S, update: Update<T>): S
7794
updateOne<S extends EntityState<T>>(
78-
state: S,
95+
state: PreventAny<S, T>,
96+
update: Update<T>
97+
): S
98+
updateOne<S extends EntityState<T>>(
99+
state: PreventAny<S, T>,
79100
update: PayloadAction<Update<T>>
80101
): S
81102

82-
updateMany<S extends EntityState<T>>(state: S, updates: Update<T>[]): S
83103
updateMany<S extends EntityState<T>>(
84-
state: S,
104+
state: PreventAny<S, T>,
105+
updates: Update<T>[]
106+
): S
107+
updateMany<S extends EntityState<T>>(
108+
state: PreventAny<S, T>,
85109
updates: PayloadAction<Update<T>[]>
86110
): S
87111

88-
upsertOne<S extends EntityState<T>>(state: S, entity: T): S
89-
upsertOne<S extends EntityState<T>>(state: S, entity: PayloadAction<T>): S
112+
upsertOne<S extends EntityState<T>>(state: PreventAny<S, T>, entity: T): S
113+
upsertOne<S extends EntityState<T>>(
114+
state: PreventAny<S, T>,
115+
entity: PayloadAction<T>
116+
): S
90117

91-
upsertMany<S extends EntityState<T>>(state: S, entities: T[]): S
92118
upsertMany<S extends EntityState<T>>(
93-
state: S,
119+
state: PreventAny<S, T>,
120+
entities: T[]
121+
): S
122+
upsertMany<S extends EntityState<T>>(
123+
state: PreventAny<S, T>,
94124
entities: PayloadAction<T[]>
95125
): S
96126

97-
map<S extends EntityState<T>>(state: S, map: EntityMap<T>): S
98-
map<S extends EntityState<T>>(state: S, map: PayloadAction<EntityMap<T>>): S
127+
map<S extends EntityState<T>>(state: PreventAny<S, T>, map: EntityMap<T>): S
99128
}
100129

101130
export interface EntitySelectors<T, V> {
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import {
2+
createSlice,
3+
createEntityAdapter,
4+
EntityAdapter,
5+
ActionCreatorWithPayload,
6+
ActionCreatorWithoutPayload
7+
} from 'src'
8+
import { EntityStateAdapter, EntityId, Update } from 'src/entities/models'
9+
10+
function expectType<T>(t: T) {
11+
return t
12+
}
13+
14+
function extractReducers<T>(
15+
adapter: EntityAdapter<T>
16+
): Omit<EntityStateAdapter<T>, 'map'> {
17+
const {
18+
selectId,
19+
sortComparer,
20+
getInitialState,
21+
getSelectors,
22+
map,
23+
...rest
24+
} = adapter
25+
return rest
26+
}
27+
28+
/**
29+
* should be usable in a slice, with all the "reducer-like" functions
30+
*/
31+
{
32+
type Entity = {
33+
value: string
34+
}
35+
const adapter = createEntityAdapter<Entity>()
36+
const slice = createSlice({
37+
name: 'test',
38+
initialState: adapter.getInitialState(),
39+
reducers: {
40+
...extractReducers(adapter)
41+
}
42+
})
43+
44+
expectType<ActionCreatorWithPayload<Entity>>(slice.actions.addOne)
45+
expectType<ActionCreatorWithPayload<Entity[]>>(slice.actions.addMany)
46+
expectType<ActionCreatorWithPayload<Entity[]>>(slice.actions.setAll)
47+
expectType<ActionCreatorWithPayload<EntityId>>(slice.actions.removeOne)
48+
expectType<ActionCreatorWithPayload<EntityId[]>>(slice.actions.removeMany)
49+
expectType<ActionCreatorWithoutPayload>(slice.actions.removeAll)
50+
expectType<ActionCreatorWithPayload<Update<Entity>>>(slice.actions.updateOne)
51+
expectType<ActionCreatorWithPayload<Update<Entity>[]>>(
52+
slice.actions.updateMany
53+
)
54+
expectType<ActionCreatorWithPayload<Entity>>(slice.actions.upsertOne)
55+
expectType<ActionCreatorWithPayload<Entity[]>>(slice.actions.upsertMany)
56+
}
57+
58+
/**
59+
* should not be able to mix with a different EntityAdapter
60+
*/
61+
{
62+
type Entity = {
63+
value: string
64+
}
65+
type Entity2 = {
66+
value2: string
67+
}
68+
const adapter = createEntityAdapter<Entity>()
69+
const adapter2 = createEntityAdapter<Entity2>()
70+
createSlice({
71+
name: 'test',
72+
initialState: adapter.getInitialState(),
73+
reducers: {
74+
addOne: adapter.addOne,
75+
// typings:expect-error
76+
addOne2: adapter2.addOne
77+
}
78+
})
79+
}
80+
81+
/**
82+
* should be usable in a slice with extra properties
83+
*/
84+
{
85+
type Entity = {
86+
value: string
87+
}
88+
const adapter = createEntityAdapter<Entity>()
89+
createSlice({
90+
name: 'test',
91+
initialState: adapter.getInitialState({ extraData: 'test' }),
92+
reducers: {
93+
addOne: adapter.addOne
94+
}
95+
})
96+
}
97+
98+
/**
99+
* should not be usable in a slice with an unfitting state
100+
*/
101+
{
102+
type Entity = {
103+
value: string
104+
}
105+
const adapter = createEntityAdapter<Entity>()
106+
createSlice({
107+
name: 'test',
108+
initialState: { somethingElse: '' },
109+
reducers: {
110+
// typings:expect-error
111+
addOne: adapter.addOne
112+
}
113+
})
114+
}

0 commit comments

Comments
 (0)