Search code examples
typescriptngxs

How can NGXS actions be created and dispatched generically?


The project I'm currently working on uses NGXS. In one component there is a FormControl-factory that is used to create typed form-control instances that dispatch actions of the given type whenever the form-control's value changes:

private createControl<T, U>(actionConstructor: new (val: T) => U, initialValue: T): FormControl<T> {
  const ctrl = new FormControl<T>(initialValue);
  this.subscriptions.push(                                // subscriptions is field in the component
    ctrl.valueChanges.subscribe({
      next: val => {
        this.store.dispatch(new actionConstructor(val));  // store is field in the component
      }
    })
  );
  return ctrl;
}

The factory is then called like this:

this.setNameCtrl = this.createControl(SetName, 'Bob');

SetName is an (ngxs) action:

export class SetName {
  static readonly type = '[App] Set Name';
  constructor(public name: string) {}
}

I'm now attempting to update NGXS from 18.1.3 to 18.1.4, which among other things now disallows nullable actions being passed to dispatch(). TypeScript now tells

TS2345: Argument of type 'U' is not assignable to parameter of type 'ActionOrArrayOfActions'

and I can't for the life of me work out what I have to change to make the compiler happy. The type ActionOrArrayOfActions is part of NGXS and defined as follows:

type ActionOrArrayOfActions<T> = T extends (infer U)[] ? NonNullable<U>[] : NonNullable<T>;

Does the generic type U have to be constrained in a certain way? Any help would be much appreciated.


Solution

  • We kinda had the same issue.

    We ended up copying the ActionOrArrayOfActions definition into our own codebase, since its not (yet?) exposed from ngxs and then just defining our U to be of that type.

    In your case it would probably look something like this:

    private createControl<T, U>(actionConstructor: new (val: T) => ActionOrArrayOfActions<U>, initialValue: T): FormControl<T> {
        const ctrl = new FormControl<T>(initialValue);
        this.subscriptions.push(                                // subscriptions is field in the component
          ctrl.valueChanges.subscribe({
            next: val => {
              this.store.dispatch(new actionConstructor(val));  // store is field in the component
            }
          })
        );
        return ctrl;
      }
    

    with having

    export type ActionOrArrayOfActions<T> = T extends (infer U)[] ? NonNullable<U>[] : NonNullable<T>;
    

    defined somewhere.

    Maybe it helps :)