TypeScript实现跨函数动态关联类型 约束register与dispatch联动
TypeScript 实现 register 与 dispatch 类型联动方案
不需要复杂的类型黑魔法,优先通过小幅调整结构降低实现复杂度,以下是两种可落地的实现方式。
优先推荐:实例化事件总线方案(零额外类型声明,推导100%准确)
如果可以接受将全局函数调整为事件总线实例的形式,类型实现成本最低,不需要提前预定义任何事件类型,所有类型提示跟随注册动作自动生效。
实现代码
class EventBus<Registry extends Record<string, Record<string, (...args: any[]) => any>> = {}> { private eventStore: Registry = {} as Registry; register< ModuleName extends string, EventMap extends Record<string, (...args: any[]) => any> >(moduleName: ModuleName, events: EventMap): EventBus<Registry & Record<ModuleName, EventMap>> { (this.eventStore as any)[moduleName] = events; return this as any; } dispatch< ModuleName extends keyof Registry, EventName extends keyof Registry[ModuleName] >( moduleName: ModuleName, eventName: EventName, ...args: Registry[ModuleName][EventName] extends (...args: infer Args) => any ? Args : [] ) { return this.eventStore[moduleName]?.[eventName]?.(...args); } }
使用效果
// 先定义业务类型,无需提前关联事件系统 interface Product { id: string; name: string; } const bus = new EventBus(); // 注册Products模块事件 bus.register('Products', { add(product: Product) { console.log('新增产品', product); }, delete(productId: string) { console.log('删除产品', productId); } }); // 注册ProductGroup模块事件 bus.register('ProductGroup', { create(name: string) { console.log('创建分组', name); }, update(groupId: string, name: string) { console.log('更新分组', groupId, name); } }); // dispatch调用自动做全链路校验 bus.dispatch('Products', 'add', { id: '1', name: '测试商品' }); // 校验通过 bus.dispatch('Products', 'delete', '1'); // 校验通过 bus.dispatch('Products', 'create'); // 类型报错:Products模块无create事件 bus.dispatch('ProductGroup', 'update', 'g1', '新分组名'); // 校验通过 bus.dispatch('ProductGroup', 'add', {}); // 类型报错:ProductGroup模块无add事件
这个方案的优势:
- 多实例隔离,不同业务域可以创建独立的事件总线,不会出现类型和事件污染
- 无额外类型声明成本,所有类型通过泛型自动推导
- 类型约束覆盖三个参数:模块名只能是已注册的值、事件名只能是对应模块下已注册的值、参数自动匹配对应事件的入参类型
兼容方案:全局函数形式实现
如果必须保留全局独立的register/dispatch函数形式,可以通过TS接口声明合并实现,缺点是每次注册后需要补充对应的类型声明,否则TS无法静态收集全局注册的事件信息。
实现代码
// 全局事件注册表类型,初始为空 declare global { interface GlobalEventRegistry {} } const globalEventStore: Record<string, Record<string, (...args: any[]) => any>> = {}; export function register< ModuleName extends string, EventMap extends Record<string, (...args: any[]) => any> >(moduleName: ModuleName, events: EventMap) { globalEventStore[moduleName] = events; } export function dispatch< ModuleName extends keyof GlobalEventRegistry, EventName extends keyof GlobalEventRegistry[ModuleName] >( moduleName: ModuleName, eventName: EventName, ...args: GlobalEventRegistry[ModuleName][EventName] extends (...args: infer Args) => any ? Args : [] ) { return globalEventStore[moduleName]?.[eventName]?.(...args); }
使用方式
注册事件后需要补充类型合并声明:
interface Product { id: string; name: string; } register('Products', { add(product: Product) {}, delete(productId: string) {} }); // 补充类型到全局注册表 declare global { interface GlobalEventRegistry { Products: { add: (product: Product) => void; delete: (productId: string) => void; } } } // 调用时同样有完整类型校验 dispatch('Products', 'add', { id: '1', name: '测试商品' });
原有写法的问题
之前的尝试无法实现类型联动的核心原因有两个:
Register接口的name字段类型设为string,丢失了具体模块名的字面量类型信息,无法建立模块名和事件集合的映射RegisterTypes是提前写死的所有事件名联合类型,没有和模块名做关联,无法根据传入的模块名筛选对应合法事件,也无法匹配事件的参数类型。
内容的提问来源于stack exchange,提问作者schnetzi
相关产品推荐
相关产品推荐

