TypeScript联合类型函数签名不兼容:WebSocket跨端addEventListener调用报错
问题场景
开发跨浏览器与Node.js的TypeScript WebSocket应用时,浏览器端使用原生WebSocket API,Node.js端使用ws库。将WebSocket构造函数声明为WebSocket | import("ws")类型后,调用addEventListener出现类型错误:
示例代码
const WebSocket = typeof window == "object" ? window.WebSocket : <typeof import("ws")>(require("ws")); let socket = new WebSocket(url); socket.addEventListener("message", event => { /* ... */ });
错误信息
error TS2349: This expression is not callable.
Each member of the union type '{ (type: K, listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; } | { ...; }' has signatures, but none of those signatures are compatible with each other.
最小复现
提取类型声明后的简化代码同样报错:
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");
问题原因
TypeScript对联合类型的函数调用有严格要求:只有当联合类型中所有函数的签名都能被当前调用参数兼容时,才允许调用。这里的核心冲突点:
- 重载集合不匹配:BrowserWS的
addEventListener支持任意string类型的事件,而NodeWS只支持"message"和"close";反过来NodeWS有"close"的专属重载,BrowserWS没有。 - 参数类型差异:BrowserWS的
options参数允许boolean类型,而NodeWS只接受EventListenerOptions(AddEventListenerOptions是它的子类型,但boolean不属于这个类型体系)。
即使你调用的是两者都支持的"message"事件,TypeScript也无法找到一个能同时满足两个接口所有重载的调用方式,因此抛出错误。
类型安全的解决方法
1. 运行时类型收窄(推荐)
通过环境判断明确收窄类型,让TypeScript知道当前环境下的具体类型:
const isBrowser = typeof window === "object"; const WebSocketCtor = isBrowser ? window.WebSocket : require("ws") as typeof import("ws"); let socket: WebSocket | import("ws").WebSocket; if (isBrowser) { socket = new WebSocketCtor(url); // 这里socket被收窄为浏览器WebSocket类型,可安全调用addEventListener socket.addEventListener("message", event => { /* ... */ }); } else { socket = new WebSocketCtor(url); // Node端ws库的addEventListener调用 socket.addEventListener("message", event => { /* ... */ }); }
2. 封装统一适配器
定义一个统一的WebSocket接口,然后分别实现浏览器和Node的适配器,屏蔽底层API差异:
interface UnifiedWebSocket { onMessage(callback: (data: string | ArrayBuffer) => void): void; // 其他统一方法... } function createWebSocket(url: string): UnifiedWebSocket { if (typeof window === "object") { const ws = new window.WebSocket(url); return { onMessage(callback) { ws.addEventListener("message", event => callback(event.data)); } }; } else { const WebSocket = require("ws"); const ws = new WebSocket(url); return { onMessage(callback) { ws.addEventListener("message", event => callback(event.data)); } }; } } // 使用时无需关心底层类型 const socket = createWebSocket(url); socket.onMessage(data => { /* ... */ });
3. 类型断言(简单但需谨慎)
如果能确保当前调用在两种环境下都合法,可以用类型断言跳过检查:
(socket as WebSocket).addEventListener("message", event => { /* ... */ }); // 或者 (socket as import("ws").WebSocket).addEventListener("message", event => { /* ... */ });
注意:这种方式需要自己保证运行时的正确性,若后续API变化可能导致隐藏错误。
内容的提问来源于stack exchange,提问作者JBYoshi

