socket.io-client icon indicating copy to clipboard operation
socket.io-client copied to clipboard

socket.on cannot accept generic "extends string" parameters

Open MHebes opened this issue 2 years ago • 3 comments

Describe the bug When using a generic-extended argument as the parameter to socket.on, I cannot properly type the callback. The full error message is:

Argument of type '(data: any) => void' is not assignable to parameter of type 'FallbackToUntypedListener<E extends "connect" | "connect_error" | "disconnect" ? SocketReservedEvents[E] : E extends string ? (...args: any[]) => void : never>'.ts(2345)

To Reproduce

Please fill the following code example:

Server example omitted as this is a typecheck-only bug

Client

import { io } from "socket.io-client";

const socket = io("google.com");

// fine
socket.on("event", (data: any) => {});

// fine
type Events = "abc" | "def";
socket.on("abc" as Events, (data: any) => {});

// fine
function hookup(event: Events) {
    socket.on(event, (data: any) => {})
}

// not fine
function hookup2<E extends string>(event: E) {
    socket.on(event, (data: any) => {});
}

Expected behavior Because parameterized type E extends string, the callback should be typed normally as a user-defined callback.

Instead, it seems to be defined as never or something? Not sure about that.

Workaround:

function hookup2<E extends string>(event: E) {
    socket.on(event as string, (data: any) => {});
}

Platform:

  • Device: HP Z Book
  • OS: Windows 10

Additional context VS Code Version 1.75.0

"dependencies": {
  "socket.io-client": "^4.5.4",
  "typescript": "^4.9.5"
}

MHebes avatar Feb 06 '23 18:02 MHebes

This appears to have been introduced in [email protected]. [email protected] does not exhibit this behavior, presumably because it does not have special-case handler types for "connect", "disconnect", etc.

MHebes avatar Feb 06 '23 19:02 MHebes

Note that this also happens with a specifically typed Socket<> object:

import { io, Socket } from "socket.io-client";

interface MyEvents {
    event: (data: any) => void;
    abc: (data: any) => void;
    def: (data: any) => void;
}

const socket: Socket<MyEvents, MyEvents> = io("google.com");

// fine
socket.on("event", (data: any) => {});

// fine
type Events = "abc" | "def";
socket.on("abc" as Events, (data: any) => {});

// fine
function hookup(event: Events) {
  socket.on(event, (data: any) => {});
}

// not fine
function hookup2<E extends Events>(event: E) {
  socket.on(event, (data: any) => {});
}

MHebes avatar Feb 06 '23 19:02 MHebes

Hi! Support for typed events was added in version 4.0.0: https://github.com/socketio/socket.io-client/commit/59023657a02cf78f90522e0d7797749707ed5ed2

In your first example, TypeScript indeed chokes on E extends string:

// not fine
function hookup2<E extends string>(event: E) {
    socket.on(event, (data: any) => {});
}

// fine
function hookup3(event: string) {
    socket.on(event, (data: any) => {});
}

In your 2nd example:

function hookup2<E extends Events>(event: E) {
  socket.on(event, (data: any) => {});
}

I think TypeScript complains because event may be neither "abc" nor "def", so it can't infer the callback signature from MyEvents.

Reference: https://socket.io/docs/v4/typescript/

darrachequesne avatar Feb 08 '23 06:02 darrachequesne

Closing, as I don't think there is much we can do here. Please reopen if needed.

darrachequesne avatar Apr 08 '24 16:04 darrachequesne