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", () => {