Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

send functions should have generic types #3742

Closed
MickL opened this issue Jan 6, 2021 · 9 comments
Closed

send functions should have generic types #3742

MickL opened this issue Jan 6, 2021 · 9 comments
Labels
enhancement New feature or request
Milestone

Comments

@MickL
Copy link
Contributor

MickL commented Jan 6, 2021

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) => {});
@MickL MickL added the enhancement New feature or request label Jan 6, 2021
@MaximeKjaer
Copy link
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.

@MaximeKjaer
Copy link
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.

@darrachequesne
Copy link
Member

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

darrachequesne pushed a commit that referenced this issue Mar 9, 2021
Syntax:

```ts
interface ClientToServerEvents {
  "my-event": (a: number, b: string, c: number[]) => void;
}

interface ServerToClientEvents {
  hello: (message: string) => void;
}

const io = new Server<ClientToServerEvents, ServerToClientEvents>(httpServer);

io.emit("hello", "world");

io.on("connection", (socket) => {
  socket.on("my-event", (a, b, c) => {
    // ...
  });

  socket.emit("hello", "again");
});
```

The events are not typed by default (inferred as any), so this change
is backward compatible.

Note: we could also have reused the method here ([1]) to add types to
the EventEmitter, instead of creating a StrictEventEmitter class.

Related: #3742

[1]: https://github.com/binier/tiny-typed-emitter
darrachequesne added a commit to socketio/socket.io-client that referenced this issue Mar 10, 2021
Syntax:

```ts
interface ServerToClientEvents {
  "my-event": (a: number, b: string, c: number[]) => void;
}

interface ClientToServerEvents {
  hello: (message: string) => void;
}

const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io();

socket.emit("hello", "world");

socket.on("my-event", (a, b, c) => {
  // ...
});
```

The events are not typed by default (inferred as any), so this change
is backward compatible.

Related: socketio/socket.io#3742
@darrachequesne
Copy link
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

@darrachequesne darrachequesne added this to the 4.0.0 milestone Mar 11, 2021
@MickL
Copy link
Contributor Author

MickL commented Mar 11, 2021

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

@darrachequesne
Copy link
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;
}

@MickL
Copy link
Contributor Author

MickL commented Mar 11, 2021

I meant more like this

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

Thats how most other libraries do it.

@MaximeKjaer
Copy link
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.

@MickL
Copy link
Contributor Author

MickL commented Mar 11, 2021

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.

sunrise30 added a commit to sunrise30/socket.io-client that referenced this issue Jan 8, 2022
Syntax:

```ts
interface ServerToClientEvents {
  "my-event": (a: number, b: string, c: number[]) => void;
}

interface ClientToServerEvents {
  hello: (message: string) => void;
}

const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io();

socket.emit("hello", "world");

socket.on("my-event", (a, b, c) => {
  // ...
});
```

The events are not typed by default (inferred as any), so this change
is backward compatible.

Related: socketio/socket.io#3742
dzad pushed a commit to dzad/socket.io that referenced this issue May 29, 2023
Syntax:

```ts
interface ClientToServerEvents {
  "my-event": (a: number, b: string, c: number[]) => void;
}

interface ServerToClientEvents {
  hello: (message: string) => void;
}

const io = new Server<ClientToServerEvents, ServerToClientEvents>(httpServer);

io.emit("hello", "world");

io.on("connection", (socket) => {
  socket.on("my-event", (a, b, c) => {
    // ...
  });

  socket.emit("hello", "again");
});
```

The events are not typed by default (inferred as any), so this change
is backward compatible.

Note: we could also have reused the method here ([1]) to add types to
the EventEmitter, instead of creating a StrictEventEmitter class.

Related: socketio#3742

[1]: https://github.com/binier/tiny-typed-emitter
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants