diff --git a/modules/data/src/entity-metadata/entity-definition.ts b/modules/data/src/entity-metadata/entity-definition.ts index 193c68cc17..cd171e435f 100644 --- a/modules/data/src/entity-metadata/entity-definition.ts +++ b/modules/data/src/entity-metadata/entity-definition.ts @@ -28,7 +28,10 @@ export function createEntityDefinition( const selectId = metadata.selectId || defaultSelectId; const sortComparer = (metadata.sortComparer = metadata.sortComparer || false); - const entityAdapter = createEntityAdapter({ selectId, sortComparer }); + const entityAdapter = createEntityAdapter({ + selectId: selectId as any, + sortComparer, + }); const entityDispatcherOptions: Partial = metadata.entityDispatcherOptions || {}; diff --git a/modules/entity/spec/types/entity_adapter.spec.ts b/modules/entity/spec/types/entity_adapter.spec.ts new file mode 100644 index 0000000000..317b86c99b --- /dev/null +++ b/modules/entity/spec/types/entity_adapter.spec.ts @@ -0,0 +1,68 @@ +import { expecter } from 'ts-snippet'; +import { compilerOptions } from './utils'; + +describe('EntityAdapter Types', () => { + const expectSnippet = expecter( + (code) => ` + import { EntityState, createEntityAdapter, EntityAdapter } from '@ngrx/entity'; + + interface EntityWithStringId { + id: string; + } + + interface EntityWithNumberId { + id: number; + } + + interface EntityWithoutId { + key: number; + } + + + ${code} + `, + compilerOptions() + ); + + it('sets the id type to string when the entity has a string id', () => { + expectSnippet(` + export const adapter: EntityAdapter = createEntityAdapter(); + `).toSucceed(); + }); + + it('sets the id type to number when the entity has a number id', () => { + expectSnippet(` + export const adapter: EntityAdapter = createEntityAdapter(); + `).toSucceed(); + }); + + it('sets the id type to string when selectId returns a string', () => { + expectSnippet(` + export const adapter: EntityAdapter = createEntityAdapter({ + selectId: (entity) => entity.id.toString(), + }); + `).toSucceed(); + }); + + it('sets the id type to string | number when the entity has no id and no selectId is provided', () => { + expectSnippet(` + export const adapter: EntityAdapter = createEntityAdapter(); + `).toSucceed(); + }); + + it('sets the id type to correct type if selectId is provided', () => { + expectSnippet(` + export const adapter: EntityAdapter = createEntityAdapter({ + selectId: (entity) => entity.key.toString(), + }); + `).toSucceed(); + }); + + it('sets the id type to string when selectId returns a string', () => { + expectSnippet(` + export const adapter: EntityAdapter = createEntityAdapter({ + selectId: (entity) => entity.id.toString(), + }); + `).toSucceed(); + }); +}, 8_000); diff --git a/modules/entity/src/create_adapter.ts b/modules/entity/src/create_adapter.ts index 468fd1080c..1419239213 100644 --- a/modules/entity/src/create_adapter.ts +++ b/modules/entity/src/create_adapter.ts @@ -3,12 +3,71 @@ import { Comparer, IdSelector, EntityAdapter, + IdSelectorStr, + IdSelectorNum, } from './models'; import { createInitialStateFactory } from './entity_state'; import { createSelectorsFactory } from './state_selectors'; import { createSortedStateAdapter } from './sorted_state_adapter'; import { createUnsortedStateAdapter } from './unsorted_state_adapter'; +/** + * Creates an entity adapter with methods for managing collections of entities in state. + * + * @description + * The entity adapter provides a set of predefined reducers and selectors for managing + * normalized entity collections. It includes methods for adding, updating, removing, + * and selecting entities from state. + * + * @template T - The entity type to be managed by the adapter + * + * @param options - Configuration options for the adapter + * @param options.selectId - A function that selects the unique identifier from an entity. + * Defaults to `(entity) => entity.id` if not provided. + * @param options.sortComparer - A comparer function for sorting entities in state. + * Set to `false` (default) to maintain insertion order, or provide a comparer function + * to keep entities sorted. + * + * @returns An entity adapter containing methods for managing and selecting entity collections in state. + * + * @example + * ```typescript + * // Entity with default 'id' property + * interface User { + * id: number; + * name: string; + * } + * const userAdapter = createEntityAdapter(); + * + * // Entity with custom id field + * interface Book { + * isbn: string; + * title: string; + * } + * const bookAdapter = createEntityAdapter({ + * selectId: (book) => book.isbn, + * }); + * + * // Entity with sorting + * const sortedBookAdapter = createEntityAdapter({ + * sortComparer: (a, b) => a.title.localeCompare(b.title), + * }); + * ``` + */ +export function createEntityAdapter< + T extends { id: string | number }, +>(options?: { + sortComparer?: false | Comparer; +}): EntityAdapter; +export function createEntityAdapter(options: { + selectId: IdSelectorStr; + sortComparer?: false | Comparer; +}): EntityAdapter; +export function createEntityAdapter(options: { + selectId: IdSelectorNum; + sortComparer?: false | Comparer; +}): EntityAdapter; +export function createEntityAdapter(): EntityAdapter; export function createEntityAdapter( options: { selectId?: IdSelector; diff --git a/modules/entity/src/models.ts b/modules/entity/src/models.ts index be18b7951e..23d2d991a0 100644 --- a/modules/entity/src/models.ts +++ b/modules/entity/src/models.ts @@ -106,8 +106,15 @@ export type MemoizedEntitySelectors = { >; }; -export interface EntityAdapter extends EntityStateAdapter { - selectId: IdSelector; +export interface EntityAdapter< + T, + IdType = string | number, +> extends EntityStateAdapter { + selectId: IdType extends string + ? IdSelectorStr + : IdType extends number + ? IdSelectorNum + : unknown; sortComparer: false | Comparer; getInitialState(): EntityState; getInitialState>( diff --git a/projects/www/src/app/reference/entity/EntityAdapter.json b/projects/www/src/app/reference/entity/EntityAdapter.json index dc6d085537..b476b4ee77 100644 --- a/projects/www/src/app/reference/entity/EntityAdapter.json +++ b/projects/www/src/app/reference/entity/EntityAdapter.json @@ -12,7 +12,15 @@ "excerptTokens": [ { "kind": "Content", - "text": "interface EntityAdapter extends " + "text": "interface EntityAdapter extends " }, { "kind": "Reference", @@ -41,6 +49,17 @@ "startIndex": 0, "endIndex": 0 } + }, + { + "typeParameterName": "IdType", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } } ], "name": "EntityAdapter", @@ -355,10 +374,23 @@ "kind": "Content", "text": "selectId: " }, + { + "kind": "Content", + "text": "IdType extends string ? " + }, + { + "kind": "Reference", + "text": "IdSelectorStr", + "canonicalReference": "@ngrx/entity!~IdSelectorStr:type" + }, + { + "kind": "Content", + "text": " : " + }, { "kind": "Reference", - "text": "IdSelector", - "canonicalReference": "@ngrx/entity!IdSelector:type" + "text": "IdSelectorNum", + "canonicalReference": "@ngrx/entity!~IdSelectorNum:type" }, { "kind": "Content", @@ -375,7 +407,7 @@ "name": "selectId", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 3 + "endIndex": 6 }, "docs": { "modifiers": { @@ -451,8 +483,8 @@ ], "extendsTokenRanges": [ { - "startIndex": 1, - "endIndex": 3 + "startIndex": 3, + "endIndex": 5 } ], "docs": { diff --git a/projects/www/src/app/reference/entity/createEntityAdapter.json b/projects/www/src/app/reference/entity/createEntityAdapter.json index e2f38739a0..3e76f18983 100644 --- a/projects/www/src/app/reference/entity/createEntityAdapter.json +++ b/projects/www/src/app/reference/entity/createEntityAdapter.json @@ -12,16 +12,113 @@ "excerptTokens": [ { "kind": "Content", - "text": "declare function createEntityAdapter(options?: " + "text": "declare function createEntityAdapter(options?: " + }, + { + "kind": "Content", + "text": "{\n sortComparer?: false | " + }, + { + "kind": "Reference", + "text": "Comparer", + "canonicalReference": "@ngrx/entity!Comparer:type" + }, + { + "kind": "Content", + "text": ";\n}" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "EntityAdapter", + "canonicalReference": "@ngrx/entity!EntityAdapter:interface" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "../../dist/modules/entity/types/ngrx-entity.d.ts", + "returnTypeTokenRange": { + "startIndex": 7, + "endIndex": 9 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "options", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 6 + }, + "isOptional": true + } + ], + "typeParameters": [ + { + "typeParameterName": "T", + "constraintTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + } + ], + "name": "createEntityAdapter", + "docs": { + "modifiers": { + "isInternal": false, + "isPublic": false, + "isAlpha": false, + "isBeta": false, + "isOverride": false, + "isExperimental": false + }, + "summary": "", + "usageNotes": "", + "remarks": "", + "deprecated": "", + "returns": "", + "see": [], + "params": [] + } + }, + { + "kind": "Function", + "canonicalReference": "@ngrx/entity!createEntityAdapter:function(2)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "declare function createEntityAdapter(options: " + }, + { + "kind": "Content", + "text": "{\n selectId: " }, { "kind": "Reference", - "text": "IdSelector", - "canonicalReference": "@ngrx/entity!IdSelector:type" + "text": "IdSelectorStr", + "canonicalReference": "@ngrx/entity!~IdSelectorStr:type" }, { "kind": "Content", @@ -47,7 +144,7 @@ }, { "kind": "Content", - "text": "" + "text": "" }, { "kind": "Content", @@ -60,7 +157,7 @@ "endIndex": 9 }, "releaseTag": "Public", - "overloadIndex": 1, + "overloadIndex": 2, "parameters": [ { "parameterName": "options", @@ -68,9 +165,170 @@ "startIndex": 1, "endIndex": 6 }, - "isOptional": true + "isOptional": false + } + ], + "typeParameters": [ + { + "typeParameterName": "T", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + } + ], + "name": "createEntityAdapter", + "docs": { + "modifiers": { + "isInternal": false, + "isPublic": false, + "isAlpha": false, + "isBeta": false, + "isOverride": false, + "isExperimental": false + }, + "summary": "", + "usageNotes": "", + "remarks": "", + "deprecated": "", + "returns": "", + "see": [], + "params": [] + } + }, + { + "kind": "Function", + "canonicalReference": "@ngrx/entity!createEntityAdapter:function(3)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "declare function createEntityAdapter(options: " + }, + { + "kind": "Content", + "text": "{\n selectId: " + }, + { + "kind": "Reference", + "text": "IdSelectorNum", + "canonicalReference": "@ngrx/entity!~IdSelectorNum:type" + }, + { + "kind": "Content", + "text": ";\n sortComparer?: false | " + }, + { + "kind": "Reference", + "text": "Comparer", + "canonicalReference": "@ngrx/entity!Comparer:type" + }, + { + "kind": "Content", + "text": ";\n}" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "EntityAdapter", + "canonicalReference": "@ngrx/entity!EntityAdapter:interface" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "../../dist/modules/entity/types/ngrx-entity.d.ts", + "returnTypeTokenRange": { + "startIndex": 7, + "endIndex": 9 + }, + "releaseTag": "Public", + "overloadIndex": 3, + "parameters": [ + { + "parameterName": "options", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 6 + }, + "isOptional": false + } + ], + "typeParameters": [ + { + "typeParameterName": "T", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } } ], + "name": "createEntityAdapter", + "docs": { + "modifiers": { + "isInternal": false, + "isPublic": false, + "isAlpha": false, + "isBeta": false, + "isOverride": false, + "isExperimental": false + }, + "summary": "", + "usageNotes": "", + "remarks": "", + "deprecated": "", + "returns": "", + "see": [], + "params": [] + } + }, + { + "kind": "Function", + "canonicalReference": "@ngrx/entity!createEntityAdapter:function(4)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "declare function createEntityAdapter(): " + }, + { + "kind": "Reference", + "text": "EntityAdapter", + "canonicalReference": "@ngrx/entity!EntityAdapter:interface" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "../../dist/modules/entity/types/ngrx-entity.d.ts", + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "releaseTag": "Public", + "overloadIndex": 4, + "parameters": [], "typeParameters": [ { "typeParameterName": "T",