我正在尝试按以下方式在 Typescript 中对我的数据进行建模。主要焦点是类型
MessageHandler
,它将消息中的 type
映射到接受该消息的回调。我遵循了映射类型手册章节
type BaseMessageBody = {
msg_id?: number;
in_reply_to?: number;
};
type BaseMessage<TBody> = {
src: string;
dest: string;
body: BaseMessageBody & TBody;
};
type BroadcastMessage = BaseMessage<{
type: 'broadcast';
message: number;
}>;
type InitMessage = BaseMessage<{
type: 'init';
node_id: string;
node_ids: string[];
}>;
type Message = BroadcastMessage | InitMessage;
type Body = Message['body'];
type MessageHandler = {
[M in Message as M['body']['type']]?: (message: M) => void;
};
到目前为止一切顺利。 MessageHandler的扩展类型是
type MessageHandler = {
broadcast?: ((message: BroadcastMessage) => void) | undefined;
init?: ((message: InitMessage) => void) | undefined;
}
但是当我尝试实际使用如下类型时:
const handlers: MessageHandler = {};
export const handle = (message: Message) => {
if (message.body.type === 'init') {
console.log('Initialized');
}
const handler = handlers[message.body.type];
if (!handler) {
console.warn('Unable to handle type', message.body.type);
return;
}
handler(message); //Error here
};
我收到以下错误。不知何故,处理程序类型已转变为
const handler: (message: BroadcastMessage & InitMessage) => void
error: TS2345 [ERROR]: Argument of type 'Message' is not assignable to parameter of type 'BroadcastMessage & InitMessage'.
Type 'BroadcastMessage' is not assignable to type 'BroadcastMessage & InitMessage'.
Type 'BroadcastMessage' is not assignable to type 'InitMessage'.
Types of property 'body' are incompatible.
Type 'BaseMessageBody & { type: "broadcast"; message: number; }' is not assignable to type 'BaseMessageBody & { type: "init"; node_id: string; node_ids: string[]; }'.
Type 'BaseMessageBody & { type: "broadcast"; message: number; }' is missing the following properties from type '{ type: "init"; node_id: string; node_ids: string[]; }': node_id, node_ids
handler(message);
~~~~~~~
关于堆栈溢出有一些稍微相关的问题,但我无法通过遵循它们来解决我的问题。 这里是包含所有代码的游乐场。
TypeScript 不直接支持我所说的“相关联合”,如 microsoft/TypeScript#30581 中所述。编译器只能对像
handler(message)
这样的代码块进行一次类型检查。如果 handler
的类型是 ((message: BroadcastMessage) => void) | ((message: InitMessage) => void)
等函数的 union type,并且
message
的类型是 BroadcastMessage | InitMessage
等参数的并集,则编译器无法将其视为安全。毕竟,对于这些类型的 任意 handler
和 message
变量,允许调用可能是一个错误;如果 message
属于 InitMessage
类型,但 handler
属于 (message: BroadcastMessage) => void
类型,那么你就会遇到问题。 调用函数并集的唯一安全方法是使用其参数的交集,而不是其参数的并集。联合参数可能是联合函数参数的错误类型。
在您的情况下,当然不可能发生这种故障,因为
handler
的类型和 message
的类型是 相关,因为它们来自同一来源。但看到这一点的唯一方法是编译器是否可以针对每个可能的 handler(message)
缩小分析一次 message
。但它并没有这样做。所以你会得到这个错误。
如果您只是想抑制错误并继续,您可以使用 类型断言:
handler(message as BroadcastMessage & InitMessage); // 🤷♂️
这在技术上是一个谎言,但很容易。但这并不意味着编译器认为您所做的事情是类型安全的;如果您不小心写了类似
handler(broadcastMessageOnly as BroadcastMessage & InitMessage)
的内容(假设 broadcastMessageOnly
是 BroadcastMessage
类型而不是 InitMessage
),编译器将不会捕获该错误。但这可能并不重要,只要您确信自己已正确实施即可。
如果您关心编译器在此处验证类型安全性,那么处理相关联合的推荐方法是从联合重构,并将genericindexes重构为简单的键值对象类型或此类对象上的mappedtypes类型。此技术在 microsoft/TypeScript#47109 中有详细描述。对于您的示例,相关更改如下所示:
首先让我们创建我们要构建的基本键值类型:
interface MessageMap {
broadcast: { message: number };
init: { node_id: string; node_ids: string[] };
}
然后您可以重新定义您的
Message
类型以将特定键作为类型参数:
type Message<K extends keyof MessageMap = keyof MessageMap> =
{ [P in K]: BaseMessage<MessageMap[P] & { type: P }> }[K]
如果您需要原始消息类型,您可以恢复它们:
type BroadcastMessage = Message<"broadcast">;
// type BroadcastMessage = { src: string; dest: string;
// body: BaseMessageBody & { message: number; } & { type: "broadcast"; };
// }
type InitMessage = Message<"init">;
// type InitMessage = { src: string; dest: string; body: BaseMessageBody &
// { node_id: string; node_ids: string[]; } & { type: "init"; };
// }
并且由于
Message<K>
有一个 默认类型参数 对应于键的完整联合,因此 Message
本身就相当于您的原始版本:
type MessageTest = Message
// type MessageTest = { src: string; dest: string;
// body: BaseMessageBody & { message: number; } & { type: "broadcast"; };
// } | { src: string; dest: string; body: BaseMessageBody &
// { node_id: string; node_ids: string[]; } & { type: "init"; };
// }But
并且
MessageHandler
也可以写成MessageMap
:
type MessageHandler = {
[K in keyof MessageMap]?: (message: Message<K>) => void;
};
最后,你创建了
handle
一个接受 Message<K>
的通用函数,错误就消失了:
export const handle = <K extends keyof MessageMap>(message: Message<K>) => {
if (message.body.type === 'init') {
console.log('Initialized');
}
const handler = handlers[message.body.type];
if (!handler) {
console.warn('Unable to handle type', message.body.type);
return;
}
handler(message); // okay
// const handler: (message: Message<K>) => void
};
当您调用
handler
时,编译器将其视为类型 (message: Message<K>) => void
。它不再是函数的联合;而是函数的联合。它是一个参数类型为 Message<K>
的单个函数。并且由于 message
的类型也是 Message<K>
,因此允许调用。
这种形式值得重构吗?如果您确信您的原始版本可以工作并且将继续工作,那么断言并继续前进肯定会更容易。如果您不太有信心,那么也许这里的重构值得付出努力。这取决于您的用例。