如何在自定义useSocket Hook中正确为Socket事件添加TypeScript类型?
问题:Socket.io类型化Hook中on/off回调的类型错误
背景
已为Socket服务器和客户端定义了如下事件类型:
import { Server } from "socket.io"; import { Socket } from "socket.io-client"; import { DisconnectDescription } from "socket.io-client/build/esm/socket"; export interface ServerToClientEvents { connect: () => void; connect_error: (err: Error) => void; disconnect: (reason: Socket.DisconnectReason, description?: DisconnectDescription) => void; noArg: () => void; basicEmit: (a: number, b: string, c: Buffer) => void; withAck: (d: string, callback: (e: number) => void) => void; } export interface ClientToServerEvents { joinRoom: (name: string) => void; } interface InterServerEvents { ping: () => void; } interface SocketData { foo: string; bar: number; } export interface ServerIO extends Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData> {} export interface SocketClient extends Socket<ServerToClientEvents, ClientToServerEvents> {}
问题场景
尝试创建useSocket Hook以实现事件的类型化绑定/解绑,但on和off函数的回调始终无法通过类型检查,Hook实现代码如下:
import { useEffect, useRef } from "react"; import io from "socket.io-client"; import { ClientToServerEvents, ServerToClientEvents, SocketClient } from "../@types/socketTypesNew"; type UseSocketOptions = { userID: string; endpoint?: string }; type UseSocketReturnType = { socket: React.MutableRefObject<SocketClient | null>; emit: <T extends keyof ClientToServerEvents>(eventName: T, data: Parameters<ClientToServerEvents[T]>) => void; on: <T extends keyof ServerToClientEvents>( eventName: T, callback: (...args: Parameters<ServerToClientEvents[T]>) => void ) => void; off: <T extends keyof ServerToClientEvents>( eventName: T, callback: (...args: Parameters<ServerToClientEvents[T]>) => void ) => void; }; const useSocket = ({ userID, endpoint = "/api/socketio" }: UseSocketOptions): UseSocketReturnType => { const socket = useRef<SocketClient | null>(null); useEffect(() => { socket.current = io("/", { path: endpoint, port: 3000, closeOnBeforeunload: false, }); socket.current.on("connect", () => { socket.current?.emit("joinRoom", userID); }); socket.current.on("connect_error", console.log); return () => { socket.current?.disconnect(); }; }, [userID, endpoint]); const emit = <T extends keyof ClientToServerEvents>( eventName: T, data: Parameters<ClientToServerEvents[T]> ): void => { if (socket.current) { socket.current.emit(eventName, ...data); } }; const on = <T extends keyof ServerToClientEvents>( eventName: T, callback: (...args: Parameters<ServerToClientEvents[T]>) => void ): void => { if (socket.current) { socket.current.on(eventName, callback); // ts error on callback } }; const off = <T extends keyof ServerToClientEvents>( eventName: T, callback: (...args: Parameters<ServerToClientEvents[T]>) => void ): void => { if (socket.current) { socket.current.off(eventName, callback); // ts error on callback } }; return { socket, emit, on, off, }; }; export default useSocket;
错误信息
Argument of type '(...args: Parameters<ServerToClientEvents[T]>) => void' is not assignable to parameter of type 'FallbackToUntypedListener<T extends "connect" | "connect_error" | "disconnect" ? SocketReservedEvents[T] : T extends "connect" | "connect_error" | "disconnect" | "noArg" | "basicEmit" | "withAck" ? ServerToClientEvents[T] : never>'.ts(2345)
开发环境
- Typescript 4.7.3
- ReactJS 18.2.0
- ESLint 8.17.0
- NextJS 13.2.1
- socket.io/client 4.5.1
错误原因
- 保留事件类型冲突:
connect、connect_error、disconnect是Socket.io内置的保留事件,其类型已在SocketReservedEvents中定义。你在自定义的ServerToClientEvents中重复声明这些事件,导致TypeScript无法区分内置类型与自定义类型,引发类型推断冲突。 - 手动类型声明不匹配:你手动定义的
on/off回调类型(...args: Parameters<ServerToClientEvents[T]>) => void没有兼容Socket.io内部对保留事件的类型要求,导致回调参数无法匹配预期类型。
修复方案
方案1:移除自定义接口中的保留事件(推荐)
Socket.io已为保留事件提供默认类型,无需在自定义接口中重复声明。修改类型定义:
// 修改后的ServerToClientEvents export interface ServerToClientEvents { noArg: () => void; basicEmit: (a: number, b: string, c: Buffer) => void; withAck: (d: string, callback: (e: number) => void) => void; }
然后复用SocketClient的内置类型定义on/off,避免手动声明带来的冲突:
// 调整返回类型定义 type UseSocketReturnType = { socket: React.MutableRefObject<SocketClient | null>; emit: <T extends keyof ClientToServerEvents>(eventName: T, ...data: Parameters<ClientToServerEvents[T]>) => void; on: SocketClient["on"]; off: SocketClient["off"]; }; // 调整on/off实现 const on = socket.current?.on.bind(socket.current) || (() => {}); const off = socket.current?.off.bind(socket.current) || (() => {});
同时优化emit函数的参数传递方式,更符合Socket.io的调用习惯:
const emit = <T extends keyof ClientToServerEvents>( eventName: T, ...data: Parameters<ClientToServerEvents[T]> ): void => { socket.current?.emit(eventName, ...data); };
方案2:显式合并保留事件与自定义事件类型
若需保留自定义接口中的保留事件,可显式合并内置类型与自定义类型:
import { SocketReservedEvents } from "socket.io-client"; // 合并保留事件与自定义事件类型 type AllServerEvents = SocketReservedEvents & ServerToClientEvents; // 修改on/off类型定义 type UseSocketReturnType = { socket: React.MutableRefObject<SocketClient | null>; emit: <T extends keyof ClientToServerEvents>(eventName: T, ...data: Parameters<ClientToServerEvents[T]>) => void; on: <T extends keyof AllServerEvents>(eventName: T, callback: AllServerEvents[T]) => void; off: <T extends keyof AllServerEvents>(eventName: T, callback: AllServerEvents[T]) => void; };
此方案可解决类型冲突,但不如方案1简洁,因为Socket.io已默认处理保留事件的类型逻辑。
内容的提问来源于stack exchange,提问作者Beki
相关产品推荐
相关产品推荐

