Typescript 函数的联合类型奇怪地变成了参数的交集类型

问题描述 投票:0回答:1

我正在尝试按以下方式在 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 typescript-generics data-modeling union-types mapped-types
1个回答
5
投票

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>
,因此允许调用。

这种形式值得重构吗?如果您确信您的原始版本可以工作并且将继续工作,那么断言并继续前进肯定会更容易。如果您不太有信心,那么也许这里的重构值得付出努力。这取决于您的用例。

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.