I'm working on a project that uses both Angular v16 and ngxs v18.0.
We have several different grids that all need the same variety of actions, so we decided to try making a factory function that would spit out action class definitions with unique type
properties. The function simply returns an anonymous object with a specific interface, where each property of the object is a class definition. The code works, but I cannot figure out how to get typescript to understand that the class definitions are simply properties of the object, rather than a declared namespace it should look for in the global scope.
Here's the factory module code:
export interface GridActions {
Initialize: ActionDef,
Fetch: ActionDef,
FetchSuccess: ActionDef,
FetchFailed: ActionDef,
UpdatePagingParams: ActionDef,
ToggleRowExpanded: ActionDef,
ExpandRow: ActionDef,
CollapseRow: ActionDef,
ToggleExpandAll: ActionDef,
UpdateFilter: ActionDef,
UpdateSort: ActionDef
}
export class GridActionFactory {
cache: {[key:string]:GridActions} = {};
private static instance: GridActionFactory;
static getActions(gridName: string): GridActions {
if (!this.instance) {
this.instance = new GridActionFactory();
}
if (this.instance.cache[gridName] !== undefined) {
return this.instance.cache[gridName];
}
const newActionCollection = {
Initialize: class Initialize {
static readonly type = `[Example] Initialize ${gridName} Grid State`
},
Fetch: class Fetch {
static readonly type = `[Example] Fetch ${gridName} Grid`;
},
FetchSuccess: class FetchSuccess {
static readonly type = `[Example] Fetch ${gridName} Success`;
constructor(public payload: SomeSwaggerDTO) {}
},
FetchFailed: class FetchFailed {
static readonly type = `[Example] Fetch ${gridName} Failed`;
constructor(public error: Error) {}
},
UpdatePagingParams: class UpdatePagingParams {
static readonly type = `[Example] Update ${gridName} Paging Params`;
constructor(public pageEvent: PageEvent) {}
},
ToggleRowExpanded: class ToggleRowExpanded {
static readonly type = `[Example] Toggle ${gridName} Row`;
constructor(public transferRequestId: string) {}
},
ExpandRow: class ExpandRow {
static readonly type = `[Example] Expand ${gridName} Row`;
constructor(public transferRequestId: string) {}
},
CollapseRow: class CollapseRow {
static readonly type = `[Example] Collapse ${gridName} Row`;
constructor(public transferRequestId: string) {}
},
ToggleExpandAll: class ToggleExpandAll {
static readonly type = `[Example] Toggle ${gridName} Expand All`;
},
UpdateFilter: class UpdateFilter {
static readonly type = `[Example] Update ${gridName} Filter`;
constructor(public event: FilterChangeEvent) {}
},
UpdateSort: class UpdateSort {
static readonly type = `[Example] Update ${gridName} Sort`;
constructor(public sort: Sort) {}
}
};
this.instance.cache[gridName] = newActionCollection;
return newActionCollection;
}
}
And then we have an ExampleActions.ts
file with the following:
export const ExampleActions = {
ActionableGrid: GridActionFactory.getActions('actionable-grid'),
PendingGrid: GridActionFactory.getActions('pending-grid'),
HistoricalGrid: GridActionFactory.getActions('historical-grid')
}
And then the usage:
import { ExampleActions } from './example.actions';
const AtgActions = ExampleActions.ActionableGrid;
const HtgActions = ExampleActions.HistoricalGrid;
export const EXAMPLE_STATE_TOKEN = new StateToken<ExampleStateModel>('example');
@State({
name: EXAMPLE_STATE_TOKEN,
defaults: exampleStateModelDefaults,
})
@Injectable()
export class ExampleState {
constructor() { }
@Action(AtgActions.UpdatePagingParams)
updateActionableGridPagingParams(ctx: StateContext<ExampleStateModel>, action: AtgActions.UpdatePagingParams) {
ctx.setState(
produce(draft => {
draft.actionableGridComponent.pagingParams.pageIndex = action.pageEvent.pageIndex;
draft.actionableGridComponent.pagingParams.previousPageIndex = action.pageEvent.previousPageIndex;
draft.actionableGridComponent.pagingParams.pageSize = action.pageEvent.pageSize;
})
);
ctx.dispatch(new AtgActions.Fetch);
}
}
Using AtgActions.UpdatePagingParams
in the @Action
decorator works fine, because it's an actual run-time reference to a class definition. But the typescript parser throws a compilation error on the action
param of the action handler, complaining that it can't find the namespace "AtgActions".
I have also tried updating the GridActions
interface to this:
export interface GridActions {
Initialize: ActionDef,
Fetch: ActionDef,
FetchSuccess: ActionDef<[payload: CollectionResponseLoanTransferRequestDTO], {payload: CollectionResponseLoanTransferRequestDTO}>,
FetchFailed: ActionDef<[error: Error], {error: Error}>,
UpdatePagingParams: ActionDef<[pageEvent: PageEvent], {pageEvent: PageEvent}>,
ToggleRowExpanded: ActionDef<[transferRequestId:string], {transferRequestId: string}>,
ExpandRow: ActionDef<[transferRequestId:string], {transferRequestId: string}>,
CollapseRow: ActionDef<[transferRequestId:string], {transferRequestId: string}>,
ToggleExpandAll: ActionDef,
UpdateFilter: ActionDef<[event:FilterChangeEvent], {event: FilterChangeEvent}>,
UpdateSort: ActionDef<[sort:Sort], {sort:Sort}>
}
Based on this commit (and I'm having to look at commits for this because ngxs apparently DOES NOT HAVE API DOCUMENTATION)... and then changing the method signature to
@Action(HtgActions.UpdateFilter)
updateHistoricalGridFilters(ctx: StateContext<LoanTransfersStateModel>, action: GridActions["UpdateFilter"]) {
}
Then I no longer get a compiler error about the action
param type, but the decorator no longer works, and it always considers the action
param an array, for example
TS2339: Property event does not exist on type
ActionDef<[event: FilterChangeEvent], { event: FilterChangeEvent; }>
But the typescript parser throws a compilation error on the action param of the action handler, complaining that it can't find the namespace "AtgActions"
Yes, that's because AtgActions
is a value, not a type. I understand how this could be confusing, because classes are both:
class Foo {}
const foo: Foo = new Foo();
Classes, in addition to their runtime value, define a type that is the return type of their constructor (i.e. the type of the instances new
ed up by the class).
This is not true of objects, even objects that do nothing but map names to classes and are returned by a static method of another class. You factory does not return a class but instead an object whose values are classes, and if you want its type you'd need to use typeof
.
Changing the method signature will be necessary, but (typeof AtgActions)['UpdatePagingParams']
might be a better replacement for AtgActions.UpdatePagingParams
:
updateActionableGridPagingParams(ctx: StateContext<ExampleStateModel>, action: (typeof AtgActions)['UpdatePagingParams']) {
I have also tried updating the GridActions interface to this
You need to do that anyway if you want those to be type-safe. Looking at the definition of ActionDef those type parameters default to any
, so your original version is just filled in with a bunch of any
s.