ngrx-traits
ngrx-traits copied to clipboard
not able to create custom store features with generic that use withEntities
Currently we can not create a custom store features with generics that use entity and collection pass to the withEntities* for example
export function withEntityMethods<
Entity extends { id: string | number },
Collection extends string ,
>(
entity: Entity,
collection: Collection,
fetchEntities: () => Observable<{ entities: Entity[]; total: number }>,
) {
return signalStoreFeature(
withEntities({ entity, collection }),
withCallStatus({ initialValue: 'loading', collection: collection }),
withEntitiesLocalPagination({ pageSize: 10, entity, collection }),
withEntitiesLoadingCall({ collection, fetchEntities }),
withStateLogger({ name: collection })
);
}
We get the following error
Type 'Omit<NamedEntitySignals<Entity, Collection>, FeatureResultKeys<{ state: NamedCallStatusState<Collection>; signals: NamedCallStatusComputed<...>; methods: NamedCallStatusMethods<...>; }>> & { [K in Collection as `is${Capitalize<string & K>}Loading`]: Signal<...>; } & { [K in Collection as `is${Capitalize<string & K>}...' is not assignable to type 'NamedEntitySignals<Entity, Collection>'.
After a lot of investigation this is cause by the way the types are merged in ngrx-signals
type MergeTwoFeatureResults<First extends SignalStoreFeatureResult, Second extends SignalStoreFeatureResult> = {
state: Omit<First['state'], FeatureResultKeys<Second>>;
signals: Omit<First['signals'], FeatureResultKeys<Second>>;
methods: Omit<First['methods'], FeatureResultKeys<Second>>;
} & Second;
This is use to combine to SignalStoreFeatures and if First and Second have the same props the Second will override the First;
The following could fix the issue but it has the problem that if there is two signalstore with the same prop types, the generate prop will have both types the First and Second in an or
type MergeTwoFeatureResults<First extends SignalStoreFeatureResult, Second extends SignalStoreFeatureResult> = First & Second;
The way is currently done doesnt work I beleive because of the following bug in typescript https://github.com/Microsoft/TypeScript/issues/28884#issuecomment-448356158
The basic problem is if we have two types First and Second are merge them using the following to override props
type Combine<First,Second> = Omit<First>, keyof Second>& Second;
Then you can not cast the result to First even though it should work
Small duplication of the problem
export type EntityState<Entity> = {
entityMap: Record<string | number, Entity>;
ids: string | number[];
};
export type NamedEntityState<Entity, Collection extends string> = {
[K in keyof EntityState<Entity> as `${Collection}${Capitalize<K>}`]: EntityState<Entity>[K];
};
export type NamedCallStatusState<Prop extends string> = {
[K in Prop as `${K}CallStatus`]: 'init' | 'loading' | 'loaded';
};
export function withEntityMethods<
Entity extends { id: string | number },
const Collection extends string,
>(
entity: Entity,
collection: Collection) {
type Combine =
Omit<NamedEntityState<Entity, Collection>, keyof NamedCallStatusState<Collection>>
& NamedCallStatusState<Collection>;
// fails with: Type 'Combine' is not assignable to type 'NamedEntityState<Entity, Collection>'.
let y: NamedEntityState<Entity, Collection> = {} as unknown as Combine;
// workaround use any
let y2: NamedEntityState<Entity, any> = {} as unknown as Combine;
// next works
type Combine2 =
Omit<NamedEntityState<Entity, 'apps'>, keyof NamedCallStatusState<'apps'>>
& NamedCallStatusState<'apps'>
The only way I manage to work around this is using // @ts-ignore and a any
export function withEntityMethods<
Entity extends { id: string | number },
Collection extends string ,
>(
entity: Entity,
collection: Collection,
fetchEntities: () => Observable<{ entities: Entity[]; total: number }>,
) {
// @ts-ignore
return signalStoreFeature(
withEntities({ entity, collection }),
withCallStatus({ initialValue: 'loading', collection: collection }),
withEntitiesLocalPagination({ pageSize: 10, entity, collection }),
withEntitiesLoadingCall({ collection, fetchEntities: fetchEntities as any }),
withStateLogger({ name: collection })
);
}
There is another way is to use more any of the in the types withEntities* like
export function withEntitiesLocalPagination<
Entity extends { id: string | number },
Collection extends string,
>(config: {
pageSize?: number;
currentPage?: number;
entity: Entity;
collection?: Collection;
}): SignalStoreFeature<
{
state: NamedEntityState<Entity, any>;
signals: NamedEntitySignals<Entity, any>; // <--
methods: {};
},
{
state: NamedEntitiesPaginationLocalState<Collection>;
signals: NamedEntitiesPaginationLocalComputed<Entity, Collection>;
methods: NamedEntitiesPaginationLocalMethods<Collection>;
}
>;
But this will affect the other normal cases , because it will not error when a trait has a dependency missing like withEntitiesLocalPagination will not do a compile error if withEntities is not there , so we will depend on throwing runtime errors to indicate if a dependency is missing but I dont want to go that route, because devs could introduce errors that are only visible when running the app.
I will keep investigating options