My socket server listens for a specific set of events, and so does my socket client. Here are my socket event definitions:
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> {}
I am trying to create a useSocket hook that ensures these events are properly typed. It returns functions called on
and off
, the former sets an event listener, the latter unsets it. For some reason, I cannot get the callback functions on both functions properly typed. Here is how I implemented it:
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;
The error is 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)
Why does this happen? How do I fix it?
I faced exactly the same issue today and here is a solution good enough for me:
We need to import some types:
import { DefaultEventsMap, EventNames, EventsMap } from "@socket.io/component-emitter";
Add a generic parameter to the SocketClient
type to pass the server event map:
export interface SocketClient<SE> extends Socket<SE, ClientToServerEvents> {}
The same thing with UseSocketReturnType
.
Note using EventNames
utility instead of keyof
and full callback
type coming from the event map:
type UseSocketReturnType<SE> = {
socket: React.MutableRefObject<SocketClient<SE> | null>;
emit: <T extends keyof ClientToServerEvents>(eventName: T, data: Parameters<ClientToServerEvents[T]>) => void;
on: <T extends EventNames<SE>>(
eventName: T,
callback: SE[T]
) => void;
off: <T extends EventNames<SE>>(
eventName: T,
callback: SE[T]
) => void;
};
Add appropriate changes to the hook
const useSocket = <SE extends EventsMap = DefaultEventsMap>({
userID,
endpoint = "/api/socketio",
}: UseSocketOptions): UseSocketReturnType<SE> => {
const socket = useRef<SocketClient<SE> | null>(null);
// useEffect ...
// emit ...
const on = <T extends EventNames<SE>>(
eventName: T,
callback: SE[T]
): void => {
if (socket.current) {
socket.current.on(eventName, callback); // OK
}
};
const off = <T extends EventNames<SE>>(
eventName: T,
callback: SE[T]
): void => {
if (socket.current) {
socket.current.off(eventName, callback); // OK
}
};
// return ...
};
Now you can use the hook with your events map:
const socket = useSocket<ServerToClientEvents>({ userID: 'foo' });
And you'll have proper autocomplete for on
and off
functions:
on(
eventName:
| "connect"
| "connect_error"
| "disconnect"
| "noArg"
| "basicEmit"
| "withAck",
callback:
| (() => void)
| ((err: Error) => void)
| ((reason: Socket.DisconnectReason, description?: DisconnectDescription | undefined) => void)
| (() => void)
| ((a: number, b: string, c: Buffer) => void)
| ((d: string, callback: (e: number) => void) => void)
): void
Here is a complete example: TS Playground
Solved it by allowing any event names
interface ServerToClientEvents {
// Allow any event names
[k: string]: (val: any) => any;
}
interface ClientToServerEvents {
// Allow any event names
[k: string]: (val: any) => any;
}
export const socket: Socket<ServerToClientEvents, ClientToServerEvents> =
io(SOCKETS_URL);
export const SocketContext = React.createContext(socket);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With