泛型约束Record键的TypeScript类报错问题求助
TypeScript泛型事件总线类的类型错误解析与修复
问题描述
我想实现一个内部用对象存储数据的事件总线类,核心思路是通过泛型约束Record的键类型,传入枚举来限定所有可能的事件名。编写代码后遇到两个类型错误:
- 初始化
store为{}时,报错:Type '{}' is not assignable to type 'Record<T, Map<string, (val?: string | undefined) => unknown>>' 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
相关产品推荐
相关产品推荐

