fix(sio): allow emitWithAck() for events with void callbacks (#5453)

EventNamesWithAck previously excluded events whose callback had no
non-error arguments (e.g. `(cb: () => void) => void` or
`(cb: (err: Error) => void) => void`). This made it impossible to use
emitWithAck as a simple acknowledgement mechanism without data.

The fix removes the FirstNonErrorArg void check while keeping the guard
against events with no parameters at all, so events like `() => void`
(no callback) are still correctly excluded.

Related: https://github.com/socketio/socket.io/issues/5257
This commit is contained in:
Varun Chawla
2026-03-17 10:56:40 -07:00
committed by GitHub
parent e6c722edbe
commit 8b93a18681
2 changed files with 14 additions and 17 deletions

View File

@@ -22,8 +22,6 @@ export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol);
/**
* Returns a union type containing all the keys of an event map that have an acknowledgement callback.
*
* That also have *some* data coming in.
*/
export type EventNamesWithAck<
Map extends EventsMap,
@@ -32,11 +30,11 @@ export type EventNamesWithAck<
Last<Parameters<Map[K]>> | Map[K],
K,
K extends (
Last<Parameters<Map[K]>> extends (...args: any[]) => any
? FirstNonErrorArg<Last<Parameters<Map[K]>>> extends void
? never
: K
: never
Parameters<Map[K]> extends never[]
? never
: Last<Parameters<Map[K]>> extends (...args: any[]) => any
? K
: never
)
? K
: never

View File

@@ -265,15 +265,11 @@ describe("server", () => {
interface ServerToClientEventsWithMultipleWithAck {
ackFromServer: (a: boolean, b: string) => Promise<boolean[]>;
ackFromServerSingleArg: (a: boolean, b: string) => Promise<string[]>;
// This should technically be `undefined[]`, but this doesn't work currently *only* with emitWithAck
// you can use an empty callback with emit, but not emitWithAck
onlyCallback: () => Promise<undefined>;
}
interface ServerToClientEventsWithAck {
ackFromServer: (a: boolean, b: string) => Promise<boolean>;
ackFromServerSingleArg: (a: boolean, b: string) => Promise<string>;
// This doesn't work currently *only* with emitWithAck
// you can use an empty callback with emit, but not emitWithAck
onlyCallback: () => Promise<undefined>;
}
describe("Emitting Types", () => {
@@ -420,8 +416,9 @@ describe("server", () => {
sio.timeout(0).emitWithAck("noArgs");
// @ts-expect-error - "helloFromServer" doesn't have a callback and is thus excluded
sio.timeout(0).emitWithAck("helloFromServer");
// @ts-expect-error - "onlyCallback" doesn't have a callback and is thus excluded
sio.timeout(0).emitWithAck("onlyCallback");
expectType<
ToEmitWithAck<ServerToClientEventsWithMultipleWithAck, "onlyCallback">
>(sio.timeout(0).emitWithAck<"onlyCallback">);
expectType<
ToEmitWithAck<
ServerToClientEventsWithMultipleWithAck,
@@ -496,10 +493,12 @@ describe("server", () => {
s.emitWithAck("noArgs");
// @ts-expect-error - "helloFromServer" doesn't have a callback and is thus excluded
s.emitWithAck("helloFromServer");
// @ts-expect-error - "onlyCallback" doesn't have a callback and is thus excluded
s.emitWithAck("onlyCallback");
// @ts-expect-error - "onlyCallback" doesn't have a callback and is thus excluded
s.timeout(0).emitWithAck("onlyCallback");
expectType<
ToEmitWithAck<ServerToClientEventsWithAck, "onlyCallback">
>(s.emitWithAck<"onlyCallback">);
expectType<
ToEmitWithAck<ServerToClientEventsWithAck, "onlyCallback">
>(s.timeout(0).emitWithAck<"onlyCallback">);
expectType<
ToEmitWithAck<ServerToClientEventsWithAck, "ackFromServerSingleArg">
>(s.emitWithAck<"ackFromServerSingleArg">);