socket.io icon indicating copy to clipboard operation
socket.io copied to clipboard

`socket.io` `Socket` class lacks covariance on generic `ListenEvents` and `EmitEvents` types

Open jsamr opened this issue 2 years ago • 4 comments

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.

jsamr avatar Dec 03 '22 15:12 jsamr

Hi! Thanks for the detailed report :+1:

Would you have the time to open a pull request with all necessary changes? Thanks!

darrachequesne avatar Dec 08 '22 06:12 darrachequesne

@darrachequesne Happy to give it a try when I find time!

jsamr avatar Dec 08 '22 14:12 jsamr

@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 avatar Jun 21 '23 12:06 darrachequesne

@darrachequesne Appreciate the feedback; I corrected the original post and updated the repro link.

jsamr avatar Jun 21 '23 12:06 jsamr