Skip to content

feat(signals): infer and narrow EntityId type in withEntities #4737

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions modules/signals/entities/spec/types/entity-config.types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('entityConfig', () => {
selectId: selectId2,
});

const selectId3: SelectEntityId<User> = (user) => user.key;
const selectId3: SelectEntityId<User, number> = (user) => user.key;
const userConfig3 = entityConfig({
entity: type<User>(),
selectId: selectId3,
Expand All @@ -67,15 +67,15 @@ describe('entityConfig', () => {
expectSnippet(snippet).toSucceed();
expectSnippet(snippet).toInfer(
'userConfig1',
'{ entity: User; selectId: SelectEntityId<NoInfer<User>>; }'
'{ entity: User; selectId: SelectEntityId<NoInfer<User>, number>; }'
);
expectSnippet(snippet).toInfer(
'userConfig2',
'{ entity: User; selectId: SelectEntityId<NoInfer<User>>; }'
'{ entity: User; selectId: SelectEntityId<NoInfer<User>, number>; }'
);
expectSnippet(snippet).toInfer(
'userConfig3',
'{ entity: User; selectId: SelectEntityId<NoInfer<User>>; }'
'{ entity: User; selectId: SelectEntityId<NoInfer<User>, number>; }'
);
});

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

expectSnippet(`
const selectId: SelectEntityId<{ key: string }> = (entity) => entity.key;
const selectId: SelectEntityId<{ key: string }, string> = (entity) => entity.key;

const userConfig = entityConfig({
entity: type<User>(),
Expand All @@ -121,7 +121,7 @@ describe('entityConfig', () => {
selectId: selectId2,
});

const selectId3: SelectEntityId<User> = (user) => user.key;
const selectId3: SelectEntityId<User, number> = (user) => user.key;
const userConfig3 = entityConfig({
entity: type<User>(),
collection: 'user',
Expand All @@ -132,15 +132,15 @@ describe('entityConfig', () => {
expectSnippet(snippet).toSucceed();
expectSnippet(snippet).toInfer(
'userConfig1',
'{ entity: User; collection: "user"; selectId: SelectEntityId<NoInfer<User>>; }'
'{ entity: User; collection: "user"; selectId: SelectEntityId<NoInfer<User>, number>; }'
);
expectSnippet(snippet).toInfer(
'userConfig2',
'{ entity: User; collection: "user"; selectId: SelectEntityId<NoInfer<User>>; }'
'{ entity: User; collection: "user"; selectId: SelectEntityId<NoInfer<User>, number>; }'
);
expectSnippet(snippet).toInfer(
'userConfig3',
'{ entity: User; collection: "user"; selectId: SelectEntityId<NoInfer<User>>; }'
'{ entity: User; collection: "user"; selectId: SelectEntityId<NoInfer<User>, number>; }'
);
});

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

expectSnippet(`
const selectId: SelectEntityId<{ key: string }> = (entity) => entity.key;
const selectId: SelectEntityId<{ key: string }, string> = (entity) => entity.key;

const userConfig = entityConfig({
entity: type<User>(),
Expand Down
18 changes: 11 additions & 7 deletions modules/signals/entities/src/entity-config.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import { SelectEntityId } from './models';
import { EntityId, SelectEntityId } from './models';

export function entityConfig<Entity, Collection extends string>(config: {
export function entityConfig<
Entity,
Collection extends string,
Id extends EntityId
>(config: {
entity: Entity;
collection: Collection;
selectId: SelectEntityId<NoInfer<Entity>>;
selectId: SelectEntityId<NoInfer<Entity>, Id>;
}): typeof config;
export function entityConfig<Entity>(config: {
export function entityConfig<Entity, Id extends EntityId>(config: {
entity: Entity;
selectId: SelectEntityId<NoInfer<Entity>>;
selectId: SelectEntityId<NoInfer<Entity>, Id>;
}): typeof config;
export function entityConfig<Entity, Collection extends string>(config: {
entity: Entity;
collection: Collection;
}): typeof config;
export function entityConfig<Entity>(config: { entity: Entity }): typeof config;
export function entityConfig<Entity>(config: {
export function entityConfig<Entity, Id extends EntityId>(config: {
entity: Entity;
collection?: string;
selectId?: SelectEntityId<Entity>;
selectId?: SelectEntityId<Entity, Id>;
}): typeof config {
return config;
}
33 changes: 17 additions & 16 deletions modules/signals/entities/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import {
} from './models';

declare const ngDevMode: unknown;
const defaultSelectId: SelectEntityId<{ id: EntityId }> = (entity) => entity.id;
const defaultSelectId: SelectEntityId<{ id: EntityId }, EntityId> = (entity) =>
entity.id;

export function getEntityIdSelector(config?: {
selectId?: SelectEntityId<any>;
}): SelectEntityId<any> {
selectId?: SelectEntityId<any, EntityId>;
}): SelectEntityId<any, EntityId> {
return config?.selectId ?? defaultSelectId;
}

Expand All @@ -37,15 +38,15 @@ export function cloneEntityState(
entityMapKey: string;
idsKey: string;
}
): EntityState<any> {
): EntityState<any, EntityId> {
return {
entityMap: { ...state[stateKeys.entityMapKey] },
ids: [...state[stateKeys.idsKey]],
};
}

export function getEntityUpdaterResult(
state: EntityState<any>,
state: EntityState<any, EntityId>,
stateKeys: {
entityMapKey: string;
idsKey: string;
Expand All @@ -69,9 +70,9 @@ export function getEntityUpdaterResult(
}

export function addEntityMutably(
state: EntityState<any>,
state: EntityState<any, EntityId>,
entity: any,
selectId: SelectEntityId<any>
selectId: SelectEntityId<any, EntityId>
): DidMutate {
const id = selectId(entity);

Expand All @@ -86,9 +87,9 @@ export function addEntityMutably(
}

export function addEntitiesMutably(
state: EntityState<any>,
state: EntityState<any, EntityId>,
entities: any[],
selectId: SelectEntityId<any>
selectId: SelectEntityId<any, EntityId>
): DidMutate {
let didMutate = DidMutate.None;

Expand All @@ -104,9 +105,9 @@ export function addEntitiesMutably(
}

export function setEntityMutably(
state: EntityState<any>,
state: EntityState<any, EntityId>,
entity: any,
selectId: SelectEntityId<any>
selectId: SelectEntityId<any, EntityId>
): DidMutate {
const id = selectId(entity);

Expand All @@ -122,9 +123,9 @@ export function setEntityMutably(
}

export function setEntitiesMutably(
state: EntityState<any>,
state: EntityState<any, EntityId>,
entities: any[],
selectId: SelectEntityId<any>
selectId: SelectEntityId<any, EntityId>
): DidMutate {
let didMutate = DidMutate.None;

Expand All @@ -142,7 +143,7 @@ export function setEntitiesMutably(
}

export function removeEntitiesMutably(
state: EntityState<any>,
state: EntityState<any, EntityId>,
idsOrPredicate: EntityId[] | EntityPredicate<any>
): DidMutate {
const ids = Array.isArray(idsOrPredicate)
Expand All @@ -165,10 +166,10 @@ export function removeEntitiesMutably(
}

export function updateEntitiesMutably(
state: EntityState<any>,
state: EntityState<any, EntityId>,
idsOrPredicate: EntityId[] | EntityPredicate<any>,
changes: EntityChanges<any>,
selectId: SelectEntityId<any>
selectId: SelectEntityId<any, EntityId>
): DidMutate {
const ids = Array.isArray(idsOrPredicate)
? idsOrPredicate
Expand Down
23 changes: 16 additions & 7 deletions modules/signals/entities/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@ import { Signal } from '@angular/core';

export type EntityId = string | number;

export type EntityMap<Entity> = Record<EntityId, Entity>;
export type EntityMap<Entity, Id extends EntityId> = Record<Id, Entity>;

export type EntityState<Entity> = {
entityMap: EntityMap<Entity>;
ids: EntityId[];
export type EntityState<Entity, Id extends EntityId> = {
entityMap: EntityMap<Entity, Id>;
ids: Id[];
};

export type NamedEntityState<Entity, Collection extends string> = {
[K in keyof EntityState<Entity> as `${Collection}${Capitalize<K>}`]: EntityState<Entity>[K];
export type NamedEntityState<
Entity,
Collection extends string,
Id extends EntityId
> = {
[K in keyof EntityState<
Entity,
Id
> as `${Collection}${Capitalize<K>}`]: EntityState<Entity, Id>[K];
};

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

export type SelectEntityId<Entity> = (entity: Entity) => EntityId;
export type SelectEntityId<Entity, Id extends EntityId> = (
entity: Entity
) => Id;

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

Expand Down
37 changes: 24 additions & 13 deletions modules/signals/entities/src/updaters/add-entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,39 @@ import {
getEntityUpdaterResult,
} from '../helpers';

export function addEntities<Entity extends { id: EntityId }>(
entities: Entity[]
): PartialStateUpdater<EntityState<Entity>>;
export function addEntities<Entity, Collection extends string>(
export function addEntities<
Entity extends { id: EntityId },
Id extends EntityId = Entity extends { id: infer E } ? E : never
>(entities: Entity[]): PartialStateUpdater<EntityState<Entity, Id>>;
export function addEntities<
Entity,
Collection extends string,
Id extends EntityId
>(
entities: Entity[],
config: { collection: Collection; selectId: SelectEntityId<NoInfer<Entity>> }
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
config: {
collection: Collection;
selectId: SelectEntityId<NoInfer<Entity>, Id>;
}
): PartialStateUpdater<NamedEntityState<Entity, Collection, Id>>;
export function addEntities<
Entity extends { id: EntityId },
Collection extends string
Collection extends string,
Id extends EntityId = Entity extends { id: infer E } ? E : never
>(
entities: Entity[],
config: { collection: Collection }
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
export function addEntities<Entity>(
): PartialStateUpdater<NamedEntityState<Entity, Collection, Id>>;
export function addEntities<Entity, Id extends EntityId>(
entities: Entity[],
config: { selectId: SelectEntityId<NoInfer<Entity>> }
): PartialStateUpdater<EntityState<Entity>>;
config: { selectId: SelectEntityId<NoInfer<Entity>, Id> }
): PartialStateUpdater<EntityState<Entity, Id>>;
export function addEntities(
entities: any[],
config?: { collection?: string; selectId?: SelectEntityId<any> }
): PartialStateUpdater<EntityState<any> | NamedEntityState<any, string>> {
config?: { collection?: string; selectId?: SelectEntityId<any, EntityId> }
): PartialStateUpdater<
EntityState<any, EntityId> | NamedEntityState<any, string, EntityId>
> {
const selectId = getEntityIdSelector(config);
const stateKeys = getEntityStateKeys(config);

Expand Down
37 changes: 24 additions & 13 deletions modules/signals/entities/src/updaters/add-entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,39 @@ import {
getEntityUpdaterResult,
} from '../helpers';

export function addEntity<Entity extends { id: EntityId }>(
entity: Entity
): PartialStateUpdater<EntityState<Entity>>;
export function addEntity<Entity, Collection extends string>(
export function addEntity<
Entity extends { id: EntityId },
Id extends EntityId = Entity extends { id: infer E } ? E : never
>(entity: Entity): PartialStateUpdater<EntityState<Entity, Id>>;
export function addEntity<
Entity,
Collection extends string,
Id extends EntityId
>(
entity: Entity,
config: { collection: Collection; selectId: SelectEntityId<NoInfer<Entity>> }
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
config: {
collection: Collection;
selectId: SelectEntityId<NoInfer<Entity>, Id>;
}
): PartialStateUpdater<NamedEntityState<Entity, Collection, Id>>;
export function addEntity<
Entity extends { id: EntityId },
Collection extends string
Collection extends string,
Id extends EntityId = Entity extends { id: infer E } ? E : never
>(
entity: Entity,
config: { collection: Collection }
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
export function addEntity<Entity>(
): PartialStateUpdater<NamedEntityState<Entity, Collection, Id>>;
export function addEntity<Entity, Id extends EntityId>(
entity: Entity,
config: { selectId: SelectEntityId<NoInfer<Entity>> }
): PartialStateUpdater<EntityState<Entity>>;
config: { selectId: SelectEntityId<NoInfer<Entity>, Id> }
): PartialStateUpdater<EntityState<Entity, Id>>;
export function addEntity(
entity: any,
config?: { collection?: string; selectId?: SelectEntityId<any> }
): PartialStateUpdater<EntityState<any> | NamedEntityState<any, string>> {
config?: { collection?: string; selectId?: SelectEntityId<any, EntityId> }
): PartialStateUpdater<
EntityState<any, EntityId> | NamedEntityState<any, string, EntityId>
> {
const selectId = getEntityIdSelector(config);
const stateKeys = getEntityStateKeys(config);

Expand Down
10 changes: 6 additions & 4 deletions modules/signals/entities/src/updaters/remove-all-entities.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { PartialStateUpdater } from '@ngrx/signals';
import { EntityState, NamedEntityState } from '../models';
import { EntityId, EntityState, NamedEntityState } from '../models';
import { getEntityStateKeys } from '../helpers';

export function removeAllEntities(): PartialStateUpdater<EntityState<any>>;
export function removeAllEntities(): PartialStateUpdater<EntityState<any, any>>;
export function removeAllEntities<Collection extends string>(config: {
collection: Collection;
}): PartialStateUpdater<NamedEntityState<any, Collection>>;
}): PartialStateUpdater<NamedEntityState<any, Collection, any>>;
export function removeAllEntities(config?: {
collection?: string;
}): PartialStateUpdater<EntityState<any> | NamedEntityState<any, string>> {
}): PartialStateUpdater<
EntityState<any, EntityId> | NamedEntityState<any, string, EntityId>
> {
const stateKeys = getEntityStateKeys(config);

return () => ({
Expand Down
Loading