TypeScript如何从类型推导字面量值消除MessageHub冗余参数
核心结论
TypeScript 的所有类型信息仅存在于编译阶段,编译为 JS 后会被完全擦除,因此无法直接从泛型参数中提取运行时可用的name值,不过可以通过调整设计完全消除你提到的冗余问题,不用同时指定泛型和name参数。
方案1:利用自动类型推导(最轻量改造)
你当前的代码本身就支持 TS 自动推导泛型,根本不需要显式传入泛型参数,传入name后 TS 会自动匹配对应的消息类型,完全没有冗余:
interface Message { name: "ping" | "pong"; } interface PingMessage extends Message { name: "ping"; value: string; } interface PongMessage extends Message { name: "pong"; value: string; } type HubMessage = PingMessage | PongMessage; type MessageHandler<T extends HubMessage> = (message: T) => any | Promise<any>; // 先定义消息名和消息类型的映射关系 type MessageTypeMap = { [K in HubMessage["name"]]: Extract<HubMessage, {name: K}> }; class MessageHub { private recipients: { [K in keyof MessageTypeMap]?: MessageHandler<MessageTypeMap[K]>[] } = {}; public receive<K extends keyof MessageTypeMap>( name: K, handler: MessageHandler<MessageTypeMap[K]> ) { this.recipients[name] = [...(this.recipients[name] ?? []), handler]; } public send(message: HubMessage) { (this.recipients[message.name] ?? []).forEach((handler) => handler(message) ); } } const hub = new MessageHub(); // 无需显式传泛型,自动推导消息类型 hub.receive("ping", (m) => console.log(`pinging ${m.value}`)); // m 自动识别为 PingMessage 类型 hub.receive("ping", (m) => console.log(`pinging too ${m.value}`)); hub.receive("pong", (m) => console.log(`ponging ${m.value}`)); // m 自动识别为 PongMessage 类型 hub.send({ name: "ping", value: "fly there" }); hub.send({ name: "pong", value: "and back" });
方案2:完全去掉name参数
如果你希望注册监听时连字符串name都不用传,可以用类来定义消息类型,利用运行时存在的类构造函数来提取消息标识:
// 用类定义消息,运行时保留构造函数信息 class PingMessage { readonly name = "ping" as const; constructor(public value: string) {} } class PongMessage { readonly name = "pong" as const; constructor(public value: string) {} } type HubMessage = PingMessage | PongMessage; type MessageHandler<T extends HubMessage> = (message: T) => any | Promise<any>; class MessageHub { private recipients: Record<string, MessageHandler<any>[]> = {}; public receive<T extends new (...args: any[]) => HubMessage>( messageCls: T, handler: MessageHandler<InstanceType<T>> ) { // 可以给消息类加静态name属性避免生成实例,这里是示例写法 const msgName = new messageCls("").name; this.recipients[msgName] = [...(this.recipients[msgName] ?? []), handler]; } public send(message: HubMessage) { (this.recipients[message.name] ?? []).forEach((handler) => handler(message) ); } } const hub = new MessageHub(); // 注册时仅需传消息类,无需传name也无需指定泛型 hub.receive(PingMessage, (m) => console.log(`pinging ${m.value}`)); hub.receive(PongMessage, (m) => console.log(`ponging ${m.value}`)); hub.send(new PingMessage("fly there")); hub.send(new PongMessage("and back"));
内容的提问来源于stack exchange,提问作者Tadeáš Peták
相关产品推荐
相关产品推荐

