Skip to content

Commit

Permalink
fix(typings): properly type emits with timeout (#1570)
Browse files Browse the repository at this point in the history
When emitting with a timeout (added in version 4.4.0), the "err"
argument was not properly typed and would require to split the client
and server typings. It will now be automatically inferred as an Error
object.

Workaround for previous versions:

```ts
type WithTimeoutAck<isEmitter extends boolean, args extends any[]> = isEmitter extends true ? [Error, ...args] : args;

interface ClientToServerEvents<isEmitter extends boolean = false> {
    withAck: (data: { argName: boolean }, callback: (...args: WithTimeoutAck<isEmitter, [string]>) => void) => void;
}

interface ServerToClientEvents<isEmitter extends boolean = false> {

}

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

io.on("connection", (socket) => {
    socket.on("withAck", (val, cb) => {
        cb("123");
    });
});

const socket: Socket<ServerToClientEvents, ClientToServerEvents<true>> = ioc("http://localhost:3000");

socket.timeout(100).emit("withAck", { argName: true }, (err, val) => {
  // ...
});
```

Related: #1555
  • Loading branch information
engina authored and darrachequesne committed Jan 16, 2023
1 parent 18c6e0a commit 33e4172
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 1 deletion.
38 changes: 37 additions & 1 deletion lib/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,40 @@ import debugModule from "debug"; // debug()

const debug = debugModule("socket.io-client:socket"); // debug()

type Last<T extends any[]> = T extends [...infer I, infer L] ? L : any;
type AllButLast<T extends any[]> = T extends [...infer I, infer L] ? I : any[];

type PrependTimeoutError<T> = T extends (...args: infer Params) => infer Result
? (err: Error, ...args: Params) => Result
: T;

/**
* Utility type to decorate the acknowledgement callbacks with a timeout error.
*
* This is needed because the timeout() flag breaks the symmetry between the sender and the receiver:
*
* @example
* interface Events {
* "my-event": (val: string) => void;
* }
*
* socket.on("my-event", (cb) => {
* cb("123"); // one single argument here
* });
*
* socket.timeout(1000).emit("my-event", (err, val) => {
* // two arguments there (the "err" argument is not properly typed)
* });
*
*/
export type DecorateAcknowledgements<E> = {
[K in keyof E]: E[K] extends (...args: infer Params) => infer Result
? (
...args: [...AllButLast<Params>, PrependTimeoutError<Last<Params>>]
) => Result
: E[K];
};

export interface SocketOptions {
/**
* the authentication payload sent when connecting to the Namespace
Expand Down Expand Up @@ -677,7 +711,9 @@ export class Socket<
*
* @returns self
*/
public timeout(timeout: number): this {
public timeout(
timeout: number
): Socket<ListenEvents, DecorateAcknowledgements<EmitEvents>> {
this.flags.timeout = timeout;
return this;
}
Expand Down
34 changes: 34 additions & 0 deletions test/typed-events.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ describe("typed events", () => {

socket.emit("random", 1, "2", [3]);
socket.emit("no parameters");

socket.emit("ackFromClient", "1", 2, (c, d) => {
expectType<any>(c);
expectType<any>(d);
});

socket.timeout(1000).emit("ackFromClient", "1", 2, (err, c, d) => {
expectType<any>(err);
expectType<any>(c);
expectType<any>(d);
});
});
});
});
Expand Down Expand Up @@ -111,6 +122,12 @@ describe("typed events", () => {
describe("listen and emit event maps", () => {
interface ClientToServerEvents {
helloFromClient: (message: string) => void;
ackFromClient: (
a: string,
b: number,
ack: (c: string, d: boolean) => void
) => void;
ackFromClientNoArg: (ack: () => void) => void;
}

interface ServerToClientEvents {
Expand Down Expand Up @@ -142,6 +159,23 @@ describe("typed events", () => {
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io();

socket.emit("helloFromClient", "hi");

socket.emit("ackFromClient", "1", 2, (c, d) => {
expectType<string>(c);
expectType<boolean>(d);
});

socket.timeout(1000).emit("ackFromClient", "1", 2, (err, c, d) => {
expectType<Error>(err);
expectType<string>(c);
expectType<boolean>(d);
});

socket.emit("ackFromClientNoArg", () => {});

socket.timeout(1000).emit("ackFromClientNoArg", (err) => {
expectType<Error>(err);
});
});

it("does not accept arguments of wrong types", () => {
Expand Down

0 comments on commit 33e4172

Please sign in to comment.