Skip to content

Commit ae7eb49

Browse files
feat(signals): infer and narrow EntityId type in withEntities
1 parent a23a0a1 commit ae7eb49

16 files changed

+373
-188
lines changed

Diff for: modules/signals/entities/spec/types/entity-config.types.spec.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe('entityConfig', () => {
5757
selectId: selectId2,
5858
});
5959
60-
const selectId3: SelectEntityId<User> = (user) => user.key;
60+
const selectId3: SelectEntityId<User, number> = (user) => user.key;
6161
const userConfig3 = entityConfig({
6262
entity: type<User>(),
6363
selectId: selectId3,
@@ -67,15 +67,15 @@ describe('entityConfig', () => {
6767
expectSnippet(snippet).toSucceed();
6868
expectSnippet(snippet).toInfer(
6969
'userConfig1',
70-
'{ entity: User; selectId: SelectEntityId<NoInfer<User>>; }'
70+
'{ entity: User; selectId: SelectEntityId<NoInfer<User>, number>; }'
7171
);
7272
expectSnippet(snippet).toInfer(
7373
'userConfig2',
74-
'{ entity: User; selectId: SelectEntityId<NoInfer<User>>; }'
74+
'{ entity: User; selectId: SelectEntityId<NoInfer<User>, number>; }'
7575
);
7676
expectSnippet(snippet).toInfer(
7777
'userConfig3',
78-
'{ entity: User; selectId: SelectEntityId<NoInfer<User>>; }'
78+
'{ entity: User; selectId: SelectEntityId<NoInfer<User>, number>; }'
7979
);
8080
});
8181

@@ -97,7 +97,7 @@ describe('entityConfig', () => {
9797
`).toFail(/No overload matches this call/);
9898

9999
expectSnippet(`
100-
const selectId: SelectEntityId<{ key: string }> = (entity) => entity.key;
100+
const selectId: SelectEntityId<{ key: string }, string> = (entity) => entity.key;
101101
102102
const userConfig = entityConfig({
103103
entity: type<User>(),
@@ -121,7 +121,7 @@ describe('entityConfig', () => {
121121
selectId: selectId2,
122122
});
123123
124-
const selectId3: SelectEntityId<User> = (user) => user.key;
124+
const selectId3: SelectEntityId<User, number> = (user) => user.key;
125125
const userConfig3 = entityConfig({
126126
entity: type<User>(),
127127
collection: 'user',
@@ -132,15 +132,15 @@ describe('entityConfig', () => {
132132
expectSnippet(snippet).toSucceed();
133133
expectSnippet(snippet).toInfer(
134134
'userConfig1',
135-
'{ entity: User; collection: "user"; selectId: SelectEntityId<NoInfer<User>>; }'
135+
'{ entity: User; collection: "user"; selectId: SelectEntityId<NoInfer<User>, number>; }'
136136
);
137137
expectSnippet(snippet).toInfer(
138138
'userConfig2',
139-
'{ entity: User; collection: "user"; selectId: SelectEntityId<NoInfer<User>>; }'
139+
'{ entity: User; collection: "user"; selectId: SelectEntityId<NoInfer<User>, number>; }'
140140
);
141141
expectSnippet(snippet).toInfer(
142142
'userConfig3',
143-
'{ entity: User; collection: "user"; selectId: SelectEntityId<NoInfer<User>>; }'
143+
'{ entity: User; collection: "user"; selectId: SelectEntityId<NoInfer<User>, number>; }'
144144
);
145145
});
146146

@@ -164,7 +164,7 @@ describe('entityConfig', () => {
164164
`).toFail(/No overload matches this call/);
165165

166166
expectSnippet(`
167-
const selectId: SelectEntityId<{ key: string }> = (entity) => entity.key;
167+
const selectId: SelectEntityId<{ key: string }, string> = (entity) => entity.key;
168168
169169
const userConfig = entityConfig({
170170
entity: type<User>(),

Diff for: modules/signals/entities/src/entity-config.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
1-
import { SelectEntityId } from './models';
1+
import { EntityId, SelectEntityId } from './models';
22

3-
export function entityConfig<Entity, Collection extends string>(config: {
3+
export function entityConfig<
4+
Entity,
5+
Collection extends string,
6+
Id extends EntityId
7+
>(config: {
48
entity: Entity;
59
collection: Collection;
6-
selectId: SelectEntityId<NoInfer<Entity>>;
10+
selectId: SelectEntityId<NoInfer<Entity>, Id>;
711
}): typeof config;
8-
export function entityConfig<Entity>(config: {
12+
export function entityConfig<Entity, Id extends EntityId>(config: {
913
entity: Entity;
10-
selectId: SelectEntityId<NoInfer<Entity>>;
14+
selectId: SelectEntityId<NoInfer<Entity>, Id>;
1115
}): typeof config;
1216
export function entityConfig<Entity, Collection extends string>(config: {
1317
entity: Entity;
1418
collection: Collection;
1519
}): typeof config;
1620
export function entityConfig<Entity>(config: { entity: Entity }): typeof config;
17-
export function entityConfig<Entity>(config: {
21+
export function entityConfig<Entity, Id extends EntityId>(config: {
1822
entity: Entity;
1923
collection?: string;
20-
selectId?: SelectEntityId<Entity>;
24+
selectId?: SelectEntityId<Entity, Id>;
2125
}): typeof config {
2226
return config;
2327
}

Diff for: modules/signals/entities/src/helpers.ts

+17-16
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import {
88
} from './models';
99

1010
declare const ngDevMode: unknown;
11-
const defaultSelectId: SelectEntityId<{ id: EntityId }> = (entity) => entity.id;
11+
const defaultSelectId: SelectEntityId<{ id: EntityId }, EntityId> = (entity) =>
12+
entity.id;
1213

1314
export function getEntityIdSelector(config?: {
14-
selectId?: SelectEntityId<any>;
15-
}): SelectEntityId<any> {
15+
selectId?: SelectEntityId<any, EntityId>;
16+
}): SelectEntityId<any, EntityId> {
1617
return config?.selectId ?? defaultSelectId;
1718
}
1819

@@ -37,15 +38,15 @@ export function cloneEntityState(
3738
entityMapKey: string;
3839
idsKey: string;
3940
}
40-
): EntityState<any> {
41+
): EntityState<any, EntityId> {
4142
return {
4243
entityMap: { ...state[stateKeys.entityMapKey] },
4344
ids: [...state[stateKeys.idsKey]],
4445
};
4546
}
4647

4748
export function getEntityUpdaterResult(
48-
state: EntityState<any>,
49+
state: EntityState<any, EntityId>,
4950
stateKeys: {
5051
entityMapKey: string;
5152
idsKey: string;
@@ -69,9 +70,9 @@ export function getEntityUpdaterResult(
6970
}
7071

7172
export function addEntityMutably(
72-
state: EntityState<any>,
73+
state: EntityState<any, EntityId>,
7374
entity: any,
74-
selectId: SelectEntityId<any>
75+
selectId: SelectEntityId<any, EntityId>
7576
): DidMutate {
7677
const id = selectId(entity);
7778

@@ -86,9 +87,9 @@ export function addEntityMutably(
8687
}
8788

8889
export function addEntitiesMutably(
89-
state: EntityState<any>,
90+
state: EntityState<any, EntityId>,
9091
entities: any[],
91-
selectId: SelectEntityId<any>
92+
selectId: SelectEntityId<any, EntityId>
9293
): DidMutate {
9394
let didMutate = DidMutate.None;
9495

@@ -104,9 +105,9 @@ export function addEntitiesMutably(
104105
}
105106

106107
export function setEntityMutably(
107-
state: EntityState<any>,
108+
state: EntityState<any, EntityId>,
108109
entity: any,
109-
selectId: SelectEntityId<any>
110+
selectId: SelectEntityId<any, EntityId>
110111
): DidMutate {
111112
const id = selectId(entity);
112113

@@ -122,9 +123,9 @@ export function setEntityMutably(
122123
}
123124

124125
export function setEntitiesMutably(
125-
state: EntityState<any>,
126+
state: EntityState<any, EntityId>,
126127
entities: any[],
127-
selectId: SelectEntityId<any>
128+
selectId: SelectEntityId<any, EntityId>
128129
): DidMutate {
129130
let didMutate = DidMutate.None;
130131

@@ -142,7 +143,7 @@ export function setEntitiesMutably(
142143
}
143144

144145
export function removeEntitiesMutably(
145-
state: EntityState<any>,
146+
state: EntityState<any, EntityId>,
146147
idsOrPredicate: EntityId[] | EntityPredicate<any>
147148
): DidMutate {
148149
const ids = Array.isArray(idsOrPredicate)
@@ -165,10 +166,10 @@ export function removeEntitiesMutably(
165166
}
166167

167168
export function updateEntitiesMutably(
168-
state: EntityState<any>,
169+
state: EntityState<any, EntityId>,
169170
idsOrPredicate: EntityId[] | EntityPredicate<any>,
170171
changes: EntityChanges<any>,
171-
selectId: SelectEntityId<any>
172+
selectId: SelectEntityId<any, EntityId>
172173
): DidMutate {
173174
const ids = Array.isArray(idsOrPredicate)
174175
? idsOrPredicate

Diff for: modules/signals/entities/src/models.ts

+16-7
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@ import { Signal } from '@angular/core';
22

33
export type EntityId = string | number;
44

5-
export type EntityMap<Entity> = Record<EntityId, Entity>;
5+
export type EntityMap<Entity, Id extends EntityId> = Record<Id, Entity>;
66

7-
export type EntityState<Entity> = {
8-
entityMap: EntityMap<Entity>;
9-
ids: EntityId[];
7+
export type EntityState<Entity, Id extends EntityId> = {
8+
entityMap: EntityMap<Entity, Id>;
9+
ids: Id[];
1010
};
1111

12-
export type NamedEntityState<Entity, Collection extends string> = {
13-
[K in keyof EntityState<Entity> as `${Collection}${Capitalize<K>}`]: EntityState<Entity>[K];
12+
export type NamedEntityState<
13+
Entity,
14+
Collection extends string,
15+
Id extends EntityId
16+
> = {
17+
[K in keyof EntityState<
18+
Entity,
19+
Id
20+
> as `${Collection}${Capitalize<K>}`]: EntityState<Entity, Id>[K];
1421
};
1522

1623
export type EntityProps<Entity> = {
@@ -21,7 +28,9 @@ export type NamedEntityProps<Entity, Collection extends string> = {
2128
[K in keyof EntityProps<Entity> as `${Collection}${Capitalize<K>}`]: EntityProps<Entity>[K];
2229
};
2330

24-
export type SelectEntityId<Entity> = (entity: Entity) => EntityId;
31+
export type SelectEntityId<Entity, Id extends EntityId> = (
32+
entity: Entity
33+
) => Id;
2534

2635
export type EntityPredicate<Entity> = (entity: Entity) => boolean;
2736

Diff for: modules/signals/entities/src/updaters/add-entities.ts

+24-13
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,39 @@ import {
1313
getEntityUpdaterResult,
1414
} from '../helpers';
1515

16-
export function addEntities<Entity extends { id: EntityId }>(
17-
entities: Entity[]
18-
): PartialStateUpdater<EntityState<Entity>>;
19-
export function addEntities<Entity, Collection extends string>(
16+
export function addEntities<
17+
Entity extends { id: EntityId },
18+
Id extends EntityId = Entity extends { id: infer E } ? E : never
19+
>(entities: Entity[]): PartialStateUpdater<EntityState<Entity, Id>>;
20+
export function addEntities<
21+
Entity,
22+
Collection extends string,
23+
Id extends EntityId
24+
>(
2025
entities: Entity[],
21-
config: { collection: Collection; selectId: SelectEntityId<NoInfer<Entity>> }
22-
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
26+
config: {
27+
collection: Collection;
28+
selectId: SelectEntityId<NoInfer<Entity>, Id>;
29+
}
30+
): PartialStateUpdater<NamedEntityState<Entity, Collection, Id>>;
2331
export function addEntities<
2432
Entity extends { id: EntityId },
25-
Collection extends string
33+
Collection extends string,
34+
Id extends EntityId = Entity extends { id: infer E } ? E : never
2635
>(
2736
entities: Entity[],
2837
config: { collection: Collection }
29-
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
30-
export function addEntities<Entity>(
38+
): PartialStateUpdater<NamedEntityState<Entity, Collection, Id>>;
39+
export function addEntities<Entity, Id extends EntityId>(
3140
entities: Entity[],
32-
config: { selectId: SelectEntityId<NoInfer<Entity>> }
33-
): PartialStateUpdater<EntityState<Entity>>;
41+
config: { selectId: SelectEntityId<NoInfer<Entity>, Id> }
42+
): PartialStateUpdater<EntityState<Entity, Id>>;
3443
export function addEntities(
3544
entities: any[],
36-
config?: { collection?: string; selectId?: SelectEntityId<any> }
37-
): PartialStateUpdater<EntityState<any> | NamedEntityState<any, string>> {
45+
config?: { collection?: string; selectId?: SelectEntityId<any, EntityId> }
46+
): PartialStateUpdater<
47+
EntityState<any, EntityId> | NamedEntityState<any, string, EntityId>
48+
> {
3849
const selectId = getEntityIdSelector(config);
3950
const stateKeys = getEntityStateKeys(config);
4051

Diff for: modules/signals/entities/src/updaters/add-entity.ts

+24-13
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,39 @@ import {
1313
getEntityUpdaterResult,
1414
} from '../helpers';
1515

16-
export function addEntity<Entity extends { id: EntityId }>(
17-
entity: Entity
18-
): PartialStateUpdater<EntityState<Entity>>;
19-
export function addEntity<Entity, Collection extends string>(
16+
export function addEntity<
17+
Entity extends { id: EntityId },
18+
Id extends EntityId = Entity extends { id: infer E } ? E : never
19+
>(entity: Entity): PartialStateUpdater<EntityState<Entity, Id>>;
20+
export function addEntity<
21+
Entity,
22+
Collection extends string,
23+
Id extends EntityId
24+
>(
2025
entity: Entity,
21-
config: { collection: Collection; selectId: SelectEntityId<NoInfer<Entity>> }
22-
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
26+
config: {
27+
collection: Collection;
28+
selectId: SelectEntityId<NoInfer<Entity>, Id>;
29+
}
30+
): PartialStateUpdater<NamedEntityState<Entity, Collection, Id>>;
2331
export function addEntity<
2432
Entity extends { id: EntityId },
25-
Collection extends string
33+
Collection extends string,
34+
Id extends EntityId = Entity extends { id: infer E } ? E : never
2635
>(
2736
entity: Entity,
2837
config: { collection: Collection }
29-
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
30-
export function addEntity<Entity>(
38+
): PartialStateUpdater<NamedEntityState<Entity, Collection, Id>>;
39+
export function addEntity<Entity, Id extends EntityId>(
3140
entity: Entity,
32-
config: { selectId: SelectEntityId<NoInfer<Entity>> }
33-
): PartialStateUpdater<EntityState<Entity>>;
41+
config: { selectId: SelectEntityId<NoInfer<Entity>, Id> }
42+
): PartialStateUpdater<EntityState<Entity, Id>>;
3443
export function addEntity(
3544
entity: any,
36-
config?: { collection?: string; selectId?: SelectEntityId<any> }
37-
): PartialStateUpdater<EntityState<any> | NamedEntityState<any, string>> {
45+
config?: { collection?: string; selectId?: SelectEntityId<any, EntityId> }
46+
): PartialStateUpdater<
47+
EntityState<any, EntityId> | NamedEntityState<any, string, EntityId>
48+
> {
3849
const selectId = getEntityIdSelector(config);
3950
const stateKeys = getEntityStateKeys(config);
4051

Diff for: modules/signals/entities/src/updaters/remove-all-entities.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { PartialStateUpdater } from '@ngrx/signals';
2-
import { EntityState, NamedEntityState } from '../models';
2+
import { EntityId, EntityState, NamedEntityState } from '../models';
33
import { getEntityStateKeys } from '../helpers';
44

5-
export function removeAllEntities(): PartialStateUpdater<EntityState<any>>;
5+
export function removeAllEntities(): PartialStateUpdater<EntityState<any, any>>;
66
export function removeAllEntities<Collection extends string>(config: {
77
collection: Collection;
8-
}): PartialStateUpdater<NamedEntityState<any, Collection>>;
8+
}): PartialStateUpdater<NamedEntityState<any, Collection, any>>;
99
export function removeAllEntities(config?: {
1010
collection?: string;
11-
}): PartialStateUpdater<EntityState<any> | NamedEntityState<any, string>> {
11+
}): PartialStateUpdater<
12+
EntityState<any, EntityId> | NamedEntityState<any, string, EntityId>
13+
> {
1214
const stateKeys = getEntityStateKeys(config);
1315

1416
return () => ({

0 commit comments

Comments
 (0)