Skip to content
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

not able to create custom store features with generic that use withEntities #92

Open
gabrielguerrero opened this issue Jun 5, 2024 · 0 comments

Comments

@gabrielguerrero
Copy link
Owner

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
microsoft/TypeScript#28884 (comment)

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant