socket.io
socket.io copied to clipboard
`socket.io` `Socket` class lacks covariance on generic `ListenEvents` and `EmitEvents` types
Describe the bug
socket.io and socket.io-client Socket classes are not covariant on either ListenEvents or EmitEvents. Its typings are too lax, causing TS to allow variable substitutions that should be forbidden. This bug does not happen for socket.io-client Socket instance.
To Reproduce
It's a type-only bug. Easy to reproduce
With socket.io, expectation fails
This is a reproduction for EmitEvents, it's trivial to reproduce it for ListenEvents
Socket.IO version: 4.5.4
import { Socket as ServerSocket } from "socket.io";
type EmptyRecord = Record<never, never>;
type Foo = { foo: string };
type Bar = { bar: string };
type FooEvents = {
event: (arg: Foo) => void;
};
type BarEvents = {
event: (arg: Bar) => void;
};
declare const foo: FooEvents;
// @ts-expect-error - BarEvents is not a base type for FooEvents
const fooBis: FooEvents = foo satisfies BarEvents;
// EXPECTATION FAILS WITH socket.io
type ServerSocketEmitFoo = ServerSocket<EmptyRecord, FooEvents, EmptyRecord, never>;
type ServerSocketEmitBar = ServerSocket<EmptyRecord, BarEvents, EmptyRecord, never>;
declare const socketFoo: ServerSocket<EmptyRecord, FooEvents, EmptyRecord, never>;
// @ts-expect-error - ServerSocketEmitBar should not be a base type for ServerSocketEmitFoo
const socketFooBis: ServerSocketEmitFoo = socketFoo satisfies ServerSocketEmitBar
See reproduction in TypeScript playground.
With socket.io-client, expectation ~succeeds~ fails
Socket.IO client version: 4.5.4
This code compiles fine:
import { Socket as ClientSocket } from "socket.io-client";
type EmptyRecord = Record<never, never>;
type Foo = { foo: string };
type Bar = { bar: string };
type FooEvents = {
event: (arg: Foo) => void;
};
type BarEvents = {
event: (arg: Bar) => void;
};
declare const foo: FooEvents;
// @ts-expect-error - BarEvents is not a base type for FooEvents
const fooBis: FooEvents = foo satisfies BarEvents;
// EXPECTATION SUCCEEDS WITH socket.io-client
type ClientSocketEmitFoo = ClientSocket<EmptyRecord, FooEvents>;
type ClientSocketEmitBar = ClientSocket<EmptyRecord, BarEvents>;
declare const clientSocketFoo: ClientSocket<EmptyRecord, FooEvents>;
// @ts-expect-error - ClientSocketEmitBar is not a base type for ClientSocketEmitFoo
const clientSocketFooBis: ClientSocketEmitFoo = socketFoo satisfies ClientSocketEmitBar
See reproduction in TypeScript playground.
Expected behavior
socket.io and socket.io-client Socket classes should be covariant on EmitListeners.
Additional context
I suggest implementing typings tests with expect-type library.
ERRATA: Initially, I concluded that the issue only existed on socket.io because of a typo in the client example. Props to @darrachequesne for pointing out my mistake.
Hi! Thanks for the detailed report :+1:
Would you have the time to open a pull request with all necessary changes? Thanks!
@darrachequesne Happy to give it a try when I find time!
@jsamr in your client example, I think there is a typo, it should be clientSocketFoo satisfies ClientSocketEmitBar instead of socketFoo satisfies ClientSocketEmitBar.
So it seems both the client and the server show the same behavior.
@darrachequesne Appreciate the feedback; I corrected the original post and updated the repro link.