-
Example, where import Socket from 'socket.io-client';
interface BuiltInServerEvents {
message: (param: number) => void;
}
interface BuiltInClientEvents {
// ... client events
}
class Plugin<AdditionalServerEvents, AdditionalClientEvents> {
socket: Socket<BuiltInServerEvents & AdditionalServerEvents, BuiltInClientEvents & AdditionalClientEvents>;
constructor(){
socket = new Socket('https://server.com');
/*
produces intellisense / inference error:
Argument of type '(param: any) => void' is not assignable to parameter of type 'FallbackToUntypedListener<"message" extends EventNames<AdditionalServerEvents & BuiltInServerEvents> ? (AdditionalServerEvents & BuiltInServerEvents)["message"] : never>'.
*/
socket.on('message', (param) => {});
}
} Edit: one additional example that seems like it should work but results in inference error: import Socket from 'socket.io-client';
interface BuiltInServerEvents {
message: (param: number) => void;
}
interface BuiltInClientEvents {
// ... client events
}
class Plugin<ServerEvents extends BuiltInServerEvents, ClientEvents extends BuiltInClientEvents> {
socket: Socket<ServerEvents, ClientEvents>;
constructor(){
socket = new Socket('https://server.com');
/*
produces intellisense / inference error:
Argument of type '(param: any) => void' is not assignable to parameter of type 'FallbackToUntypedListener<"message" extends EventNames<T> ? T[keyof T & "message"] : never>'.ts(2345)
*/
socket.on('message', (param) => {});
}
} High level goal: class that extends Plugin has working Intellisense / inference on the base class defined event types, in addition to its own. Thank you for any help. |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 2 replies
-
Hi. There indeed seems to be an error with the types: import { io, Socket } from 'socket.io-client';
import type { EventsMap, DefaultEventsMap } from "@socket.io/component-emitter";
interface BuiltInServerEvents {
message: (param: number) => void;
}
interface BuiltInClientEvents {
// ... client events
}
class Plugin<AdditionalServerEvents extends EventsMap = DefaultEventsMap, AdditionalClientEvents extends EventsMap = DefaultEventsMap> {
socket: Socket<BuiltInServerEvents & AdditionalServerEvents, BuiltInClientEvents & AdditionalClientEvents>;
constructor(){
this.socket = io('https://server.com');
/*
produces intellisense / inference error:
Argument of type '(param: any) => void' is not assignable to parameter of type 'FallbackToUntypedListener<"message" extends EventNames<T> ? T[keyof T & "message"] : never>'.ts(2345)
*/
this.socket.on('message', (param) => {});
}
} Somehow this works when providing an explicit type: socket: Socket<BuiltInServerEvents & { test: () => void }, BuiltInClientEvents & AdditionalClientEvents>; cc @ZachHaber sorry to bother! Could you please help? |
Beta Was this translation helpful? Give feedback.
-
The issue that you are running into is that at the time typescript is trying to resolve the type of As an example of how inconsistent this is: type ServerEvents = AdditionalServerEvents & BuiltInServerEvents;
// @ts-expect-error - it doesn't know what type param is.
const test3: ServerEvents["message"] = (param) => {};
// @ts-expect-error - Even if you specify that it is a number, typescript still doesn't like it!
const test3: ServerEvents["message"] = (param: number) => {};
const test4: ServerEvents['message'] = undefined as unknown as ServerEvents['message'];
// This *should* be an error, but it's not...
test4(3,'s');
// If you create a ServerEvents variable, then boom! it all works 🤷
const test: ServerEvents = undefined as unknown as ServerEvents;
// This is not an error.
test["message"](3);
// Nor is this.
test["message"] = (param) => {};
// @ts-expect-error - using typeof the working variable is back to being an error
const test5: typeof test['message'] = (param)=>{} I don't think there's a to solve this issue at the library side in general, unless there's a whole lot of improvement in how typescript works with nested generics especially with complicated type definitions that are pulling information out of those nested generics. The good news is that there's a fairly simple workaround for the user side: TS Playground It basically boils down to this: interface BuiltInServerEvents {
message: (param: number) => void;
}
interface BuiltInClientEvents {
// ... client events
foo: (bar: number) => void;
}
interface EventsMap {
[event: string]: any;
}
class Plugin<
AdditionalServerEvents extends EventsMap,
AdditionalClientEvents extends EventsMap
> {
// Separate out the built-ins that are known at class declaration-time
private builtInSocket: Socket<BuiltInServerEvents, BuiltInClientEvents>;
// Then have another class field with the intersection between the generics and the built-ins
socket: Socket<AdditionalServerEvents & BuiltInServerEvents, AdditionalClientEvents & BuiltInClientEvents>;
constructor() {
// They can both be assigned to the same instance of Socket
// if it complains, just use a type-cast
this.builtInSocket = io("https://server.com");
this.socket = this.builtInSocket;
// No type error here, since we aren't using the class's generic parameters here!
this.builtInSocket.on("message", (param) => {});
this.builtInSocket.on("connect", () => {});
}
// Here, use the types of the public socket to define your user-facing functions
// Due to how generics work, you'll undoubtedly need to use type-casting here
on: typeof this.socket.on = (...args) => {
return this.socket.on(
...(args as Parameters<typeof this.socket.on>)
);
};
} |
Beta Was this translation helpful? Give feedback.
The issue that you are running into is that at the time typescript is trying to resolve the type of
this.socket.on
, the generics for the socket could be anything. Then due to the unresolved generic, the type parsing that's done in the library won't work correctly as those rely on the type being definable by typescript. It would be nice if typescript knew to resolve the bits it can resolve until the generic is specified and then resolves the whole thing with that generic. But given that the generic could be literally anything, even a type that is a union or an event map with conflicting values to the built-ins, it doesn't know how to fully resolve the types, so it doesn't. This leads to it…