You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Server emitWithAck on typed Server has "never" typing #5257
emitWithAck doesn't seem to be picking up types correctly, so the type variable Ev is assigned never.
To Reproduce
Socket.IO server version: 4.8.1
Server
import{Server}from"socket.io";interfaceServerToClientEvents{someEvent: (callback: (err: Error)=>void)=>void;}interfaceClientToServerEvents{}constio=newServer<ClientToServerEvents,ServerToClientEvents>();io.on("connection",async(socket)=>{// Typescript gives error "Argument of type 'string' is not assignable to parameter of type 'never'.ts(2345)"awaitsocket.emitWithAck("someEvent");// No errorsocket.emit("someEvent",(err)=>{});});
Client
No client code necessary.
Expected behavior
No Typescript error (similar to emit behavior)
Platform:
Device: Macbook Air M2
OS: MacOS Sonoma 14.6.1
Additional context
Seems to be the same bug as mentioned in the comment #4925 (comment), but different from that issue.
There is no 'non-Error parameters' so FirstNonErrorArg returns never(or undefined? anyway this does not affect the conclusion) and never extends void is true.
The compiler correctly complains there is no events available.
If it's intentional, it's unclear why you wouldn't be able to use a callback signature of (err: Error) => void (or () => void which also gives the same error). It's true that (num: number) => void does not cause the error.
Since both (err: Error) => void and () => void are unclear in perspective of "What does the event return?", I think excluding them is reasonable.
If you intended "nothing to return", you could use (err: Error, res: void) => void, which is more explicit.
I don't see how they're unclear: callback: (err: Error) => void means "the event handler will call callback(err) with an Error object" (granted, I now realize it's a little bit of a weird case because it should probably be Error | undefined to optionally pass errors back, which the emitWithAck Typescript is fine with), and callback: () => void means "the event handler will call callback() to simply ack that it has received the event".
For the latter, what's more indicative that it's a bug is that emit is perfectly happy with it, while emitWithAck is not. At the least, the behavior should be consistent.
emitWithAck (asynchronously) returns value that is specified on the callback, whereas emit does not.
// exampleimport{Server}from"socket.io"interfaceServerToClientEvents{test: (cb: (result: number)=>void)=>void}constio=newServer<{},ServerToClientEvents>()io.on("connection",socket=>{constackResult=socket.emitWithAck("test")// ackResult: Promise<number>constemitResult=socket.emit("test",()=>{})// emitResult: boolean (irrelevant to the definition of events)})
But, if there is no return type specified, what should the function return? Of course we could return undefined, but it is implicit. If the author intended to require explicit return type, this behavior is reasonable.
I understand what you're saying from the "how it's implemented" perspective, but I'm looking at it from the "what would a user expect" perspective. This is a perfectly reasonable thing to want to do and expect it will work:
Hi there! I stumbled upon this issue while working with socket.broadcast.emitWithAck, and I thought my experience could help shed some light on the situation.
In my case, I defined something like this on the server:
This typing works fine on the server side, where you're expecting both an error and a value. But when I tried to reuse the same types on the client side, it became problematic: the client callback signature now requires both error and value, making it very awkward to call—even when all I care about is the value.
To work around this, I created a small internal type helper that removes the Error parameter from callback types before applying them to the client:
import{EmitEvent,ListenEvents}from"@repo/socket/events";// Shared typesimport{io}from"socket.io-client";importtype{Socket}from"socket.io-client";// Simple approach: we only focus on callbacks with Error as the first paramtypeRemoveErrorFromCallback<T>=Textends(error: Error,
...args: infer Rest)=> infer Return
? (...args: Rest)=>Return
: T;// For functions that take a callback, we clean up that callbacktypeCleanCallbackFunction<T>=Textends(callback: infer CB,
...args: infer Rest)=> infer Return
? (callback: RemoveErrorFromCallback<CB>, ...args: Rest)=>Return
: T;// Apply this to the entire interfacetypeCleanErrorInEvents<E>={[KinkeyofE]: CleanCallbackFunction<E[K]>;};typeClientTypedSocket=Socket<CleanErrorInEvents<EmitEvents>,CleanErrorInEvents<ListenEvents>>;constsocket: ClientTypedSocket=io("...");
In short, this "cleans" the callback types for the client, so you don't end up with callback(error, value) enforced everywhere—you get just callback(value) instead.
I know this is not a complete fix inside the Socket.IO type definitions, but maybe this approach could help others in the meantime.
Activity
barroudjo commentedon Dec 14, 2024
I have exactly the same problem
andjsrk commentedon Jan 22, 2025
I think this is intentional, because:
Error.Error:socket.io/packages/socket.io/lib/typed-events.ts
Lines 36 to 38 in 7fcddcb
Errorparameters' soFirstNonErrorArgreturnsnever(orundefined? anyway this does not affect the conclusion) andnever extends voidis true.mrjogo commentedon Jan 22, 2025
If it's intentional, it's unclear why you wouldn't be able to use a callback signature of
(err: Error) => void(or() => voidwhich also gives the same error). It's true that(num: number) => voiddoes not cause the error.andjsrk commentedon Jan 22, 2025
Since both
(err: Error) => voidand() => voidare unclear in perspective of "What does the event return?", I think excluding them is reasonable.If you intended "nothing to return", you could use
(err: Error, res: void) => void, which is more explicit.mrjogo commentedon Jan 22, 2025
I don't see how they're unclear:
callback: (err: Error) => voidmeans "the event handler will callcallback(err)with an Error object" (granted, I now realize it's a little bit of a weird case because it should probably beError | undefinedto optionally pass errors back, which theemitWithAckTypescript is fine with), andcallback: () => voidmeans "the event handler will callcallback()to simply ack that it has received the event".For the latter, what's more indicative that it's a bug is that
emitis perfectly happy with it, whileemitWithAckis not. At the least, the behavior should be consistent.andjsrk commentedon Jan 22, 2025
emitWithAck(asynchronously) returns value that is specified on the callback, whereasemitdoes not.But, if there is no return type specified, what should the function return? Of course we could return
undefined, but it is implicit. If the author intended to require explicit return type, this behavior is reasonable.mrjogo commentedon Jan 22, 2025
I understand what you're saying from the "how it's implemented" perspective, but I'm looking at it from the "what would a user expect" perspective. This is a perfectly reasonable thing to want to do and expect it will work:
Especially since it's doable in a non-async way:
andjsrk commentedon Jan 22, 2025
(This comment is not intended to defend the current behavior)
FYI, even you write explicit
void, you don't need to write additional code on call site:null-d-e-v commentedon Aug 23, 2025
Hi there! I stumbled upon this issue while working with
socket.broadcast.emitWithAck, and I thought my experience could help shed some light on the situation.In my case, I defined something like this on the server:
This typing works fine on the server side, where you're expecting both an
errorand avalue. But when I tried to reuse the same types on the client side, it became problematic: the client callback signature now requires botherrorandvalue, making it very awkward to call—even when all I care about is thevalue.To work around this, I created a small internal type helper that removes the
Errorparameter from callback types before applying them to the client:In short, this "cleans" the callback types for the client, so you don't end up with
callback(error, value)enforced everywhere—you get justcallback(value)instead.I know this is not a complete fix inside the Socket.IO type definitions, but maybe this approach could help others in the meantime.
fix(types): allow emitWithAck for events with void callbacks
fix(sio): allow emitWithAck() for events with void callbacks (#5453)
darrachequesne commentedon Sep 25, 2026
This should be fixed by 8b93a18, included in
socket.io@4.8.4.Release notes: https://github.com/socketio/socket.io/releases/tag/socket.io%404.8.4