socket.io
socket.io copied to clipboard
Type params to Socket do not allow setting only the data type
Describe the bug
If you want to make a middleware function to check authentication and set the user in socket.data, you cannot create the proper type for it without supplying ListenEvents, EmitEvents, and ServerSideEvents, whose default types are not exported.
Here's the type from socket.io package:
export declare class Socket<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap,
SocketData = any
> extends StrictEventEmitter<ListenEvents, EmitEvents, SocketReservedEventsMap> {}
The first three type params have real defaults, while the SocketData is simply any as a default.
We have a library that exports a koa middleware function which checks authentication/authorization and sets the user object in the socket.data.user property, therefore we need to type the SocketData. Since this is a library function, we don't want to change the types for the events, instead leaving them as the defaults.
Unfortunately, the SocketData type param is last, and the default types for the other type params are not exported.
If the other default types were exported, we could do:
export type AuthenticatedSocket = Socket<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, { user: AppUser }>;
or, even better, if the params were reordered, putting SocketData first:
export declare class Socket<
SocketData = any,
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap,
> extends StrictEventEmitter<ListenEvents, EmitEvents, SocketReservedEventsMap> {}
we could simply do the following:
export type AuthenticatedSocket = Socket<{ user: AppUser }>;
This doesn't make it harder to type the Socket when you don't want to type the SocketData property, because any is a built-in type, so you could just do:
export type MySocket = Socket<any, MyTypeOne, MyTypeTwo, MyTypeThree>;
To Reproduce
Please fill the following code example:
Socket.IO version: 4.7.0
- simplified example in a Koa app *
import { type Server as HttpServer } from 'http';
import * as Koa from 'koa';
import { Server, type Socket } from 'socket.io';
type KoaListenFn = (port?: number, hostname?: string, listeningListener?: () => void) => HttpServer;
interface AppUser {
id: number;
name: string;
}
// this doesn't work because `socket.data` is now `any & { data: { user: AppUser } }` which is still just `any`
// and we can't set the fourth type param to `Socket` without recreating all the default types from scratch
type AugmentedAuthenticatedSocket = Socket & { data: { user: AppUser } };
const getSocketAuthenticateMiddleware = () => (socket: AugmentedAuthenticatedSocket, next: (err?: Error) => void): void => {
const user: AppUser = { id: 1, name: 'Some User' };
// here we get lint errors:
//
// Unsafe member access .user on an `any` value
// Unsafe assignment of an `any` value
//
// since socket.data is not properly typed
socket.data.user = {
...socket.data.user,
sso: user,
};
next();
};
const app: Koa & { io?: Server } = new Koa();
const originalListenFn: KoaListenFn = <KoaListenFn> app.listen.bind(app, 8080, '127.0.0.1');
app.listen = (): HttpServer => {
const server: HttpServer = originalListenFn();
const io = new Server(server);
io.on('connection', (socket: AugmentedAuthenticatedSocket): void => {
// here we get lint errors:
//
// Unsafe member access .user on an `any` value
// Invalid type "any" of template literal expression
//
// since socket.data is not properly typed
console.log(`User ${socket.data.user.sso.user.name} is connected`);
});
app.io = io;
return server;
};
app.use(getSocketAuthenticateMiddleware());
We can bandage this by recreating the default types in our code:
export interface EventsMap {
[event: string]: any;
}
interface DefaultEventsMap {
[event: string]: (...args: any[]) => void;
}
export type AuthenticatedSocket = Socket<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, { user: AppUser }>;
but this is brittle and can break if the base types change, and is unnecessary duplication.
The request here is to:
- re-order the types, putting the
any-defaultedSocketDatafirst (breaking change, so I realize this is less likely, at least in the near-term) OR - export the default type param values
Expected behavior A clear and concise description of what you expected to happen.
Platform:
- Device: [e.g. Samsung S8] n/a
- OS: [e.g. Android 9.2] n/a
Additional context Add any other context about the problem here.
Hi! Re-ordering the types is indeed a breaking change so it likely won't happen.
The workaround you suggested should work well for your use case:
interface DefaultEventsMap {
[event: string]: (...args: any[]) => void;
}
const io = new Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, { user: AppUser }>(server);
If there are enough demand, we might export the DefaultEventsMap type :+1: