You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在自定义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

错误原因

  1. 保留事件类型冲突:connect、connect_error、disconnect是Socket.io内置的保留事件,其类型已在SocketReservedEvents中定义。你在自定义的ServerToClientEvents中重复声明这些事件,导致TypeScript无法区分内置类型与自定义类型,引发类型推断冲突。
  2. 手动类型声明不匹配:你手动定义的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 06:12:02