Skip to content

send functions should have generic types #3742

Closed

Description

@MickL

Is your feature request related to a problem? Please describe.

Currently functions like emit are not typable:

socket.emit('my-event-name', dto);

This applies to both socket.io server and client.

Describe the solution you'd like
I would like to have generic types on all emit functions to (optionally) type the send-object and the response-object:

socket.emit<void, MyDtoInterface>('my-event-name', dto);
socket.emit<MyResponseDtoInterface, MyDtoInterface>('my-event-name', dto, responseDto => {
   // ...
});

Now if an interface changes TypeScript throws an error if the objects are not updated.

Suggested solution
Functions could look something like this:

// Server:
emit<TInput = any>(ev: string, data: TInput);

// Client:
emit<TResult = any, TInput = any>(ev: string, data: TInput, (response: TResult) => {});

Activity

  1. MaximeKjaer commented on Feb 26, 2021

    @MaximeKjaer
    Contributor

    I have a WIP implementation that would fix this issue on this branch. @darrachequesne would there be interest in potentially merging something like that? If so, I can complete the implementation.

    The implementation adds two optional type parameters to Socket, Server and Namespace. These type parameter are a mapping of event names to listener types; by default, it's a mapping of all strings to (...args: any[]) => void, so it retains the current behavior if no type parameters are passed. However, users can optionally pass a mapping of their event names to the types of the listeners that listen to those events.

  2. MaximeKjaer commented on Feb 26, 2021

    @MaximeKjaer
    Contributor

    @MickL in the meantime, this library may be of interest to you: https://github.com/bterlson/strict-event-emitter-types (also see the blog post that explains the library). It allows you to cast socket.io types as strictly typed EventEmitter. The README has an example of this in action.

    But this solution is not perfect; their implementation has a thing called assignmentCompatibilityHack, which sometimes fails in weird ways. So I think that in order to properly address this issue, we need to implement some stricter event typing in socket.io.

  3. darrachequesne commented on Feb 26, 2021

    @darrachequesne
    Member

    @MaximeKjaer as long as the feature is stable and does not fail in surprising ways during compilation, that sounds interesting 😄

  4. darrachequesne commented on Mar 11, 2021

    @darrachequesne
    Member

    This was implemented by 0107510 (server) and socketio/socket.io-client@5902365 (client) and included in Socket.IO v4 🔥

    Syntax:

    interface ClientToServerEvents {
      noArg: () => void;
      basicEmit: (a: number, b: string, c: number[]) => void;
    }
    
    interface ServerToClientEvents {
      withAck: (d: string, cb: (e: number) => void) => void;
    }
    
    // client
    import { io, Socket } from "socket.io-client";
    
    const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io();
    
    // server
    import { Server } from "socket.io";
    
    const io = new Server<ClientToServerEvents, ServerToClientEvents>(3000);

    Documentation: https://socket.io/docs/v3/migrating-from-3-x-to-4-0/#Typed-events

  5. added this to the 4.0.0 milestone on Mar 11, 2021
  6. MickL commented on Mar 11, 2021

    @MickL
    ContributorAuthor

    Not exactly what I desired but it should work. I wanted to do it per function instead of per server.

  7. darrachequesne commented on Mar 11, 2021

    @darrachequesne
    Member

    @MickL if you want to only type certain events, you should be able to do:

    interface ServerToClientEvents {
      withAck: (d: string, cb: (e: number) => void) => void;
      [event: string]: (...args: any[]) => void;
    }
  8. MickL commented on Mar 11, 2021

    @MickL
    ContributorAuthor

    I meant more like this

    socket.emit<void, MyDtoInterface>('my-event-name', dto);

    Thats how most other libraries do it.

  9. MaximeKjaer commented on Mar 11, 2021

    @MaximeKjaer
    Contributor

    You could always cast the socket to a Socket<A, B> with the exact A or B event maps you need for each function call.

    But as far as I know, most type-safe event-emitter libraries actually take the same approach as what I implemented, i.e. an interface declaring all events and their types ahead of time.

  10. MickL commented on Mar 11, 2021

    @MickL
    ContributorAuthor

    For example Angular http client does it like this:

    https://github.com/angular/angular/blob/2c757591ee334ced115bc93ba1b944803bb0c07e/packages/common/http/src/client.ts#L2291

    Or all NestJS Microservice Clients:
    https://github.com/nestjs/nest/blob/eb239abd8261b3328945865790ffc3c377b60827/packages/microservices/client/client-proxy.ts#L60

    I specifically used NestJS as the source of my proposal above.

    It is easy to implement two generics, it does not add any extra code and is totally optional to use.

  11. added a commit that references this issue on Jan 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions