Type does not satisfy the constraint of class it extends

70 Views Asked by At

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?

2

There are 2 best solutions below

0
On BEST ANSWER

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
        })
    }
}

See Playground

2
On

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>) {}