You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 03:48:43