angulartypescripttypescript-typingstypescript-genericsngrx

Type inference problem with array of generic config objects with different inner types that depend on other inner property


I'm trying to create a generic function that will create some pretty common effects, so I'll be able to pass only the config and it will create the effect.

Here are some imports from NgRx:

import { ActionCreator } from '@ngrx/store';
import { createActionGroup, props } from '@ngrx/store';

and one ActionGroup for testing purposes:

export const featureActions = createActionGroup({
  source: 'MyGroup',
  events: {
    'Update Entity': props<{ data: { updateName: string } }>(),
    'Delete Entity': props<{ data: { deleteName: string } }>(),
  },
});

So to achieve that I've created a config file that accepts a ngrx action (ActionCreator) and one mapping function.

export type Config<AC extends ActionCreator> = {
  action: AC;
  mapping: (data: ReturnType<AC>) => string;
};

The factory function itself accepts the array of these configs:

export function createMyEffects<AC extends ActionCreator>(
  effects: Config<AC>[]
) {
  // doing nothing, I guess it is not relevant
  console.log(effects);
}

The issue happens on function call:

createMyEffects([
  {
    action: featureActions.updateEntity,
    mapping: (updateData) => updateData.data.updateName, // place #2
  },
  {
    // Issue is that it thinks that this action should be the same as the first
    action: featureActions.deleteEntity, // the place #1
    mapping: (deleteData) => deleteData.data.deleteName, // place #3
  },
]);

So with my function declaration TS throws me at place #1 that typeof FeatureAtions.deleteEntity isn't assignable to typeof FeatureAtions.updateEntity.

So I changed it to

export function createMyEffects<AC extends ActionCreator[]>(
  effects: Config<AC[number]>[],
)

and in this case, there are no errors, but now I'm losing the type inference at places #2 and #3. deleteData and updateData args infer type Object instead of ReturnType< FeatureAtions.deleteEntity> and ReturnType<FeatureAtions.updateEntity> corresponding.

How could I ensure the type inference in this case?

UPD: Here is stackblits link with env


Solution

  • If you want each element of your effects array input to have its own type argument to Config<>, then you will need to make the function generic in a tuple type of those type arguments, and make effects a mapped tuple type:

    function createMyEffects<AC extends ActionCreator[]>(
        effects: { [I in keyof AC]: Config<AC[I]> }
    ) {
        console.log(effects);
    }
    

    This will then work as desired, at least for TypeScript 5.4 and above:

    createMyEffects([
        {
            action: featureActions.updateEntity,
            mapping: (updateData) => updateData.data.updateName,
        },
        {
            action: featureActions.deleteEntity,
            mapping: (deleteData) => deleteData.data.deleteName,
        },
    ]);
    

    Unfortunately before TypeScript 5.4 this wouldn't work properly... TypeScript has the ability to infer from mapped types, but when the function input is an array literal this didn't work right. It was fixed in microsoft/TypeScript#56555.

    Playground link to code