如何让TypeScript识别返回动态类定义的NGXS工厂函数(Angular16)
解决NGXS动态动作工厂的TypeScript类型识别问题
问题场景
在Angular v16 + NGXS v18项目中,为多个网格组件复用动作逻辑,通过工厂函数生成带唯一type的动作类集合。代码运行正常,但TypeScript无法正确识别对象属性中的类类型:
- 直接使用
AtgActions.UpdatePagingParams作为参数类型时,TS误将AtgActions识别为命名空间,抛出找不到命名空间的错误 - 改用
ActionDef泛型定义接口后,动作参数被识别为构造参数数组,无法访问实例属性,同时NGXS装饰器失效
解决方案
核心思路是让TypeScript明确识别动作类的构造函数类型和实例类型,通过调整接口定义、工厂返回类型和使用方式,解决类型推断问题。
1. 定义动作实例类型与构造函数接口
先为每个动作定义实例类型,再构造包含构造函数签名的GridActions接口,让TS能关联构造函数和实例类型:
// 定义每个动作的实例类型 interface InitializeAction { readonly type: string; } interface FetchAction { readonly type: string; } interface FetchSuccessAction { readonly type: string; payload: SomeSwaggerDTO; } interface FetchFailedAction { readonly type: string; error: Error; } interface UpdatePagingParamsAction { readonly type: string; pageEvent: PageEvent; } // 其他动作实例类型同理定义... // 定义GridActions为构造函数集合,每个属性对应动作的构造函数签名 export interface GridActions { Initialize: new () => InitializeAction; Fetch: new () => FetchAction; FetchSuccess: new (payload: SomeSwaggerDTO) => FetchSuccessAction; FetchFailed: new (error: Error) => FetchFailedAction; UpdatePagingParams: new (pageEvent: PageEvent) => UpdatePagingParamsAction; // 其他动作的构造函数签名... }
2. 重构GridActionFactory实现
确保工厂返回的对象严格符合GridActions接口,同时为每个动作类实现对应的实例类型,让TS能正确推断实例属性:
export class GridActionFactory { private 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]) { return this.instance.cache[gridName]; } const prefix = `[Example]`; const newActionCollection: GridActions = { Initialize: class implements InitializeAction { static readonly type = `${prefix} Initialize ${gridName} Grid State`; readonly type = (this.constructor as typeof Initialize).type; }, Fetch: class implements FetchAction { static readonly type = `${prefix} Fetch ${gridName} Grid`; readonly type = (this.constructor as typeof Fetch).type; }, FetchSuccess: class implements FetchSuccessAction { static readonly type = `${prefix} Fetch ${gridName} Success`; readonly type = (this.constructor as typeof FetchSuccess).type; constructor(public payload: SomeSwaggerDTO) {} }, FetchFailed: class implements FetchFailedAction { static readonly type = `${prefix} Fetch ${gridName} Failed`; readonly type = (this.constructor as typeof FetchFailed).type; constructor(public error: Error) {} }, UpdatePagingParams: class implements UpdatePagingParamsAction { static readonly type = `${prefix} Update ${gridName} Paging Params`; readonly type = (this.constructor as typeof UpdatePagingParams).type; constructor(public pageEvent: PageEvent) {} }, // 其他动作类实现同理... }; this.instance.cache[gridName] = newActionCollection; return newActionCollection; } }
3. 正确声明动作集合的类型
使用as const保留字面量类型,同时定义类型别名方便后续使用:
// 生成具体的动作集合 export const ActionableGridActions = GridActionFactory.getActions('actionable-grid'); export const PendingGridActions = GridActionFactory.getActions('pending-grid'); export const HistoricalGridActions = GridActionFactory.getActions('historical-grid'); // 导出动作集合,用as const让TS保留完整类型信息 export const ExampleActions = { ActionableGrid: ActionableGridActions, PendingGrid: PendingGridActions, HistoricalGrid: HistoricalGridActions } as const; // 定义类型别名,简化后续类型引用 export type ActionableGridActions = typeof ActionableGridActions; export type HistoricalGridActions = typeof HistoricalGridActions;
4. 在State中正确使用类型
通过InstanceType工具类型从构造函数类型中提取实例类型,让TS正确识别动作参数的属性:
import { ExampleActions, ActionableGridActions } from './example.actions'; const AtgActions = ExampleActions.ActionableGrid; export const EXAMPLE_STATE_TOKEN = new StateToken<ExampleStateModel>('example'); @State({ name: EXAMPLE_STATE_TOKEN, defaults: exampleStateModelDefaults, }) @Injectable() export class ExampleState { @Action(AtgActions.UpdatePagingParams) updateActionableGridPagingParams( ctx: StateContext<ExampleStateModel>, action: InstanceType<ActionableGridActions['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()); } }
问题原因说明
- 最初的错误是因为TypeScript在类型位置使用对象属性的类时,默认不会将其识别为类的实例类型,需要明确通过
InstanceType提取 - 使用
ActionDef时,泛型的第一个参数是构造函数的参数数组,直接引用GridActions["UpdateFilter"]得到的是构造函数类型而非实例类型,导致无法访问实例属性
内容的提问来源于stack exchange,提问作者Isochronous
相关产品推荐
相关产品推荐

