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

泛型约束Record键的TypeScript类报错问题求助

TypeScript泛型事件总线类的类型错误解析与修复

问题描述

我想实现一个内部用对象存储数据的事件总线类,核心思路是通过泛型约束Record的键类型,传入枚举来限定所有可能的事件名。编写代码后遇到两个类型错误:

  1. 初始化store为{}时,报错:Type '{}' is not assignable to type 'Record<T, Map<string, (val?: string | undefined) => unknown>>'
  2. unsubscribe方法中,handler被推断为unknown类型,无法调用delete方法。

原代码

class BusServiceFactory<T extends string = string> {
  private readonly store: Record<T, Map<string, (val?: string) => unknown>> = {};

  subscribe(uid: string, eventName: T, callback: (val?: string) => unknown): void {
    this.store[eventName] ??= new Map();
    this.store[eventName].set(uid, callback);
  }

  unsubscribe(uid: string): void {
    Object.values(this.store).forEach((handler) => handler.delete(uid));
  }
// ... 其他方法省略
}

enum CustomEvent {
  EventA = 'evtA',
  EventB = 'evtB',
  EventC = 'evtC',
}

export const CustomBus = new BusServiceFactory<CustomEvent>();

错误原因分析

1. 空对象不满足Record<T, U>类型要求

Record<T, U>是TypeScript的工具类型,要求对象必须包含所有T类型的键,且每个键对应的值类型为U。当用CustomEvent作为泛型参数时,Record<CustomEvent, ...>要求对象必须包含evtA、evtB、evtC三个键,每个键对应一个Map实例。而初始化的{}没有任何键,完全不符合类型约束,因此报错。

2. Object.values()的类型推断局限

TypeScript对Object.values()的类型推断存在限制:当传入Record<T, U>类型的对象时,由于T是泛型参数,TypeScript无法确定T的具体取值范围,因此会将Object.values()的返回值推断为unknown[],导致遍历中的handler类型变成unknown,无法调用delete方法。

修复方案

方案1:用Partial优化store类型定义

将Record<T, ...>改为Partial<Record<T, ...>>,Partial工具类型会把所有键转为可选,这样空对象{}就符合类型要求。同时用非空断言和可选链处理可能未初始化的Map:

class BusServiceFactory<T extends string = string> {
  // Partial让所有事件键变为可选
  private readonly store: Partial<Record<T, Map<string, (val?: string) => unknown>>> = {};

  subscribe(uid: string, eventName: T, callback: (val?: string) => unknown): void {
    this.store[eventName] ??= new Map();
    // 非空断言确保this.store[eventName]已初始化
    this.store[eventName]!.set(uid, callback);
  }

  unsubscribe(uid: string): void {
    // 显式指定Object.values的返回类型,避免推断为unknown[]
    (Object.values(this.store) as Map<string, (val?: string) => unknown>[]).forEach((handler) => {
      // 可选链处理可能未初始化的Map
      handler?.delete(uid);
    });
  }
}

方案2:构造函数预初始化所有事件对应的Map

如果希望store始终包含所有枚举对应的键,可以在构造函数中传入枚举值列表,预创建每个事件对应的Map:

class BusServiceFactory<T extends string = string> {
  private readonly store: Record<T, Map<string, (val?: string) => unknown>>;

  constructor(eventNames: T[]) {
    // 用reduce初始化所有事件对应的Map
    this.store = eventNames.reduce((acc, name) => {
      acc[name] = new Map();
      return acc;
    }, {} as Record<T, Map<string, (val?: string) => unknown>>);
  }

  subscribe(uid: string, eventName: T, callback: (val?: string) => unknown): void {
    this.store[eventName].set(uid, callback);
  }

  unsubscribe(uid: string): void {
    Object.values(this.store).forEach((handler) => handler.delete(uid));
  }
}

// 传入枚举的所有值初始化总线
export const CustomBus = new BusServiceFactory(Object.values(CustomEvent));

内容的提问来源于stack exchange,提问作者Léal

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 10:15:04