Reputation: 11372
I'm working on a WSS server using the Node ws library. Specifically, using the approach described in this question which may or may not be relevant.
I built a prototype server that works perfectly, but when I try to bring the same code across to my regular project, it doesn't work.
I wanted an easy way to identify the origin server for any websocket request:
import { Server, Data, WebSocket } from "ws";
interface IIdWebSocket extends WebSocket {
id: string;
}
export class WSSServer {
private wss = new Server<IIdWebSocket>({ port: 8090 });
// and so on
Now the problem I have is that when I try to build this in my real project, I get the following message in the new Server<IIdWebSocket>
line:
src/wws-server.ts:24:30 - error TS2344: Type 'IIdWebSocket' does not satisfy the constraint 'typeof WebSocket'.
Type 'IIdWebSocket' is missing the following properties from type 'typeof WebSocket': prototype, createWebSocketStream, Server, WebSocketServer, and 9 more
I can't see an obvious difference between the version of the code that works and the version that doesn't. I notice that looking at the definitely typed source for the typescript library the property it is complaining about isn't a property of WebSocket
at all - instead it appears to be a property of the Server type.
What is happening here?
Upvotes: 1
Views: 300
Reputation: 5796
The accepted answer is really just lying to the type system. The type of the WebSocket
that is created will still be the default one, which means that even though you're claiming it has an id
property, there won't actually be one. You should make an actual subclass of WebSocket
.
// `id` will be `undefined`
wss.on('connection', (ws) => console.log(ws.id.toUpperCase())
Take note of the type parameter for the server:
class Server<
T extends typeof WebSocket.WebSocket = typeof WebSocket.WebSocket,
U extends typeof IncomingMessage = typeof IncomingMessage,
> extends EventEmitter {
It is NOT T extends WebSocket.WebSocket
, but T extends typeof WebSocket.WebSocket
. typeof SomeClass
gives you the type of the class object (constructor function), not the type of an instance of the class. Thus, after extending the original WebSocket
class, the generic type parameter should be Server<typeof CustomWebSocket>
.
Also note the constructor parameter WebSocket
. Here, you pass the custom class, which is then created here on connection:
https://github.com/websockets/ws/blob/a57e963f946860f6418baaa55b307bfa7d0bc143/lib/websocket-server.js#L382
Full example:
import { Server, WebSocket } from 'ws'
export class CustomWebSocket extends WebSocket {
userId!: string
sendStuff() {
this.send('stuff')
}
}
export class CustomWebsocketServer extends Server<typeof CustomWebSocket> {
constructor() {
super({
WebSocket: CustomWebSocket,
})
}
socketForUser(userId: string): CustomWebSocket | undefined {
// this.clients is of type Set<CustomWebSocket>
for (const socket of this.clients) {
if (socket.userId === userId) {
return socket
}
}
}
}
const wss = new CustomWebsocketServer()
wss.on('connection', (socket) => {
// socket is of type CustomWebSocket
socket.sendStuff()
})
Final note: You don't need one, but if you add a constructor to your WebSocket
subclass, it has to be compatible with that of the original WebSocket
class, which is techically:
constructor(...params: ConstructorParameters<typeof WebSocket>) {}
Upvotes: 2
Reputation: 187232
It's expecting the type of a class constructor and you are passing it an object type. The value WebSocket
is a class constructor, and so typeof WebSocket
is the type for that constructor.
type IIdWebSocketConstructor = typeof WebSocket & {
new(): IIdWebSocket
}
interface IIdWebSocket extends WebSocket {
id: string
}
This creates a constructor type that returns IIdWebSocket
when constructed.
export class WSSServer {
private wss = new Server<IIdWebSocketConstructor>({ port: 8090 });
testing123() {
this.wss.on('connection', (socket) => {
socket.id // works
// ^? number
})
}
}
Upvotes: 2