From 33e417258c9a5697e001163971ae87821e9c097f Mon Sep 17 00:00:00 2001 From: Engin Aydogan Date: Fri, 13 Jan 2023 21:50:29 +0300 Subject: [PATCH] fix(typings): properly type emits with timeout (#1570) 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 true ? [Error, ...args] : args; interface ClientToServerEvents { withAck: (data: { argName: boolean }, callback: (...args: WithTimeoutAck) => void) => void; } interface ServerToClientEvents { } const io = new Server>(3000); io.on("connection", (socket) => { socket.on("withAck", (val, cb) => { cb("123"); }); }); const socket: Socket> = ioc("http://localhost:3000"); socket.timeout(100).emit("withAck", { argName: true }, (err, val) => { // ... }); ``` Related: https://github.com/socketio/socket.io-client/issues/1555 --- lib/socket.ts | 38 ++++++++++++++++++++++++++++++++++++- test/typed-events.test-d.ts | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/lib/socket.ts b/lib/socket.ts index 621f1b9e..175998f7 100644 --- a/lib/socket.ts +++ b/lib/socket.ts @@ -12,6 +12,40 @@ import debugModule from "debug"; // debug() const debug = debugModule("socket.io-client:socket"); // debug() +type Last = T extends [...infer I, infer L] ? L : any; +type AllButLast = T extends [...infer I, infer L] ? I : any[]; + +type PrependTimeoutError = 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 = { + [K in keyof E]: E[K] extends (...args: infer Params) => infer Result + ? ( + ...args: [...AllButLast, PrependTimeoutError>] + ) => Result + : E[K]; +}; + export interface SocketOptions { /** * the authentication payload sent when connecting to the Namespace @@ -677,7 +711,9 @@ export class Socket< * * @returns self */ - public timeout(timeout: number): this { + public timeout( + timeout: number + ): Socket> { this.flags.timeout = timeout; return this; } diff --git a/test/typed-events.test-d.ts b/test/typed-events.test-d.ts index 678dc379..99d46f27 100644 --- a/test/typed-events.test-d.ts +++ b/test/typed-events.test-d.ts @@ -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(c); + expectType(d); + }); + + socket.timeout(1000).emit("ackFromClient", "1", 2, (err, c, d) => { + expectType(err); + expectType(c); + expectType(d); + }); }); }); }); @@ -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 { @@ -142,6 +159,23 @@ describe("typed events", () => { const socket: Socket = io(); socket.emit("helloFromClient", "hi"); + + socket.emit("ackFromClient", "1", 2, (c, d) => { + expectType(c); + expectType(d); + }); + + socket.timeout(1000).emit("ackFromClient", "1", 2, (err, c, d) => { + expectType(err); + expectType(c); + expectType(d); + }); + + socket.emit("ackFromClientNoArg", () => {}); + + socket.timeout(1000).emit("ackFromClientNoArg", (err) => { + expectType(err); + }); }); it("does not accept arguments of wrong types", () => {