TypeScript联合类型函数签名不兼容

yh2wf1be  于 2023-01-31  发布在  TypeScript
关注(0)|答案(1)|浏览(310)

我正在开发一个使用Web套接字的TypeScript应用程序。我试图设置可以在Web浏览器和Node.js之间重用的代码,所以我使用浏览器的原生WebSocket API和Node上的ws库。然而,两者之间仍然存在一些小的API差异。所以我声明web套接字变量为WebSocket | import("ws")类型(WebSocket是web浏览器版本),但是,当我试图调用addEventListener函数时,我得到了一个类型错误:

const WebSocket = typeof window == "object" ? window.WebSocket : <typeof import("ws")>(require("ws"));
let socket = new WebSocket(url);
socket.addEventListener("message", event => { /* ... */ });

错误消息为:
错误TS2349:此表达式不可调用。
联合类型"{(type:K、听众:(这是:WebSocket,版本:网络套接字事件Map[K])=〉任何,选项?:布尔|添加事件侦听器选项):无效;(类型:字符串,侦听器:事件侦听器或事件侦听器对象,选项?:布尔|添加事件侦听器选项):无效;}|"{...;}"具有签名,但这些签名彼此不兼容。
我提取了TypeScript的DOM库和ws库的类型声明,并得出了这个最小的概念证明,它在addEventListener调用上也失败了(注意,AddEventListenerOptionsEventListenerOptions的子类型,添加了一些可选属性):

interface BrowserWS {
    addEventListener(type: "message", options?: boolean | AddEventListenerOptions): void;
    addEventListener(type: string, options?: boolean | AddEventListenerOptions): void;
}
interface NodeWS {
    addEventListener(type: "message", options?: EventListenerOptions): void;
    addEventListener(type: "close", options?: EventListenerOptions): void;
}
let s: BrowserWS | NodeWS = {addEventListener: () => {}};
s.addEventListener("message");

我认为这会很好地工作,因为addEventListener()调用可以接受未定义的或符合AddEventListenerOptions的类型,但它也会在addEventListener上失败,并出现几乎相同的错误(只是提到的特定联合类型不同)。
为什么会发生这种情况?是否有类型安全的变通方法?

yeotifhr

yeotifhr1#

如果你有一个值f,它的类型是函数类型的并集,并且你有一个参数列表args,如果并集的每个成员都接受这个参数列表,那么 * 概念上 f(...args)应该被编译器接受。
实际上,这只是部分支持,并且取决于f的类型。在TypeScript 3.3之前,基本上没有支持,只有当f的成员基本相同时,它才能工作。
microsoft/TypeScript#29011中实现了调用联合类型的改进行为,现在您可以调用具有参数列表交集的函数类型的联合,但 * 仅 * 当最多一个联合成员是generic函数时,
仅 * 当最多一个联合成员是具有多个调用签名的重载函数时。
如以上链接的发行说明和拉取请求中所述,存在这些限制是因为在泛型面前通过编程合成合并的调用签名并不容易(在这种情况下,函数参数的交叉将无法很好地进行类型参数推断)或面对多个调用签名时(对于相对温和的工会来说,可能产生的电话数量会变得非常大)。未来可能会有改进,但目前这是极限所在。
在您的示例中,联合BrowserWS | NodeWSaddEventListener属性是函数类型的联合,* 每个 * 都被重载。由于重载了多个联合成员,因此不支持调用该联合。
至于变通方法,我能想到的唯一方法是自己手动合成合并后的签名,方法是遵循我想象的编译器在可能的情况下会遵循的规则:对来自每个联合成员的调用签名的每个可能选择的参数列表求交集,并希望与调用签名顺序没有奇怪的交互:

interface EitherWS {
    // BrowserWS 1/2 [type: "message", options?: boolean | AddEventListenerOptions]
    // NodeWS 1/2 [type: "message", options?: EventListenerOptions]
    // Joined [type: "message" & "message", 
    //    options?: (boolean | AddEventListenerOptions) & EventListenerOptions]
    addEventListener(type: "message", options?: AddEventListenerOptions): void;

    // BrowserWS 1/2 [type: "message", options?: boolean | AddEventListenerOptions]
    // NodeWS 2/2 [type: "close", options?: EventListenerOptions]
    // Joined [type: "message" & "close", 
    //    options?: AddEventListenerOptions] // impossible

    // BrowserWS 2/2 [type: string, options?: boolean | AddEventListenerOptions];
    // NodeWS 1/2 [type: "message", options?: EventListenerOptions]
    // Joined [type: string & "message", 
    //    options?: AddEventListenerOptions] // identical to first, ignore

    // BrowserWS 2/2 [type: string, options?: boolean | AddEventListenerOptions];
    // NodeWS 2/2 [type: "close", options?: EventListenerOptions]
    // Joined [type: string & "close", options?: EventListenerOptions]
    addEventListener(type: "close", options?: EventListenerOptions): void;
}

好了,我们有了一个新的类型,它有两个调用签名,编译器仍然不允许你调用它:

let s: BrowserWS | NodeWS = { addEventListener: () => { } };
s.addEventListener("message"); // error!

但是它会让你把s赋给一个EitherWS类型的新变量:

let t: EitherWS = s; // this succeeds

它确实是可调用的

t.addEventListener("message"); // okay

它并不完美编译器不能从BrowserWS | NodeWS * 生成 * EitherWS,但是它至少可以 * 验证 * 后者可以赋值给前者。2因此这个变通方法是类型安全的,虽然有点繁琐。
Playground代码链接

相关问题