PHASE 2 — Topic 8: Installing and Configuring socket.io and socket.io-client with TypeScript Types

The server already has socket.io installed and attached. In this post, we install the client library and set up proper TypeScript types on both sides, so every event we send or receive is fully type-checked.

Step 1: Install socket.io-client

Run this in your project root:


    npm install socket.io-client

That's it, no separate types package is needed. socket.io-client ships with its own TypeScript definitions built in.

Step 2: Why Typed Events Matter

By default, if you write plain Socket.IO code, emit and on accept any event name and any data, as plain strings and any. That means a typo like scoket.emit("mesage", data) would not be caught by TypeScript at all, it would just silently fail at runtime.

Socket.IO solves this by letting you define your events as TypeScript interfaces, and passing them as generic types to both the server and the client. Once set up, your editor will autocomplete event names, and TypeScript will throw an error if you send the wrong data shape.

Step 3: Create a Shared Types File

Create a new folder and file: src/types/socket.ts. This file will be imported by both the server and the client, so event definitions never go out of sync.


    export interface ServerToClientEvents {
        message: (data: { text: string; sender: string }) => void;
    }

    export interface ClientToServerEvents {
        sendMessage: (data: { text: string; sender: string }) => void;
    }

    export interface InterServerEvents {
        ping: () => void;
    }

    export interface SocketData {
        userId: string;
    }

Here is what each interface is for:

  • ServerToClientEvents — events the server sends, and the client listens for
  • ClientToServerEvents — events the client sends, and the server listens for
  • InterServerEvents — used only if you later run multiple server instances that talk to each other
  • SocketData — custom data you can attach to a socket instance, like a user ID after authentication

Step 4: Apply These Types on the Server

Open server.ts, and update the Socket.IO server setup like this:


    import { createServer } from "http";
    import { parse } from "url";
    import next from "next";
    import { Server } from "socket.io";
    import type {
        ServerToClientEvents,
        ClientToServerEvents,
        InterServerEvents,
        SocketData,
    } from "./src/types/socket";

    const dev = process.env.NODE_ENV !== "production";
    const hostname = "localhost";
    const port = parseInt(process.env.PORT || "3000", 10);

    const app = next({ dev, hostname, port });
    const handle = app.getRequestHandler();

    app.prepare().then(() => {
        const httpServer = createServer((req, res) => {
            const parsedUrl = parse(req.url!, true);
            handle(req, res, parsedUrl);
        });

        const io = new Server<
            ClientToServerEvents,
            ServerToClientEvents,
            InterServerEvents,
            SocketData
        >(httpServer, {
            path: "/api/socket",
        });

        io.on("connection", (socket) => {
            console.log("A client connected:", socket.id);

            socket.on("sendMessage", (data) => {
                console.log("Received:", data.text, "from", data.sender);
            });

            socket.on("disconnect", () => {
                console.log("Client disconnected:", socket.id);
            });
        });

        httpServer.listen(port, () => {
            console.log(`Server ready on http://${hostname}:${port}`);
        });
    });

Notice that socket.on("sendMessage", ...) now has a fully typed data parameter. If you try socket.on("sendMesage", ...), misspelled, TypeScript will immediately flag it as an error, since it does not match anything in ClientToServerEvents.

Step 5: Apply These Types on the Client

Now create the client-side connection. Create a new file: src/lib/socket.ts.


    import { io, Socket } from "socket.io-client";
    import type {
        ServerToClientEvents,
        ClientToServerEvents,
    } from "@/types/socket";

    export const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io({
        path: "/api/socket",
    });

A few important details here:

  • The generic order is reversed compared to the server: on the client, it's Socket<ListenEvents, EmitEvents>, meaning the events the client listens for come first, and the events it emits come second
  • io() is called with no URL, since the client will connect to the same host it is served from
  • The path option must exactly match what we set on the server, /api/socket, or the connection will fail

Step 6: How This Feels in Practice

Once both sides are typed, your development experience changes noticeably. For example, on the client:


    socket.emit("sendMessage", { text: "Hello", sender: "Gagan" });

If you forget the sender field, or pass a number instead of a string, TypeScript will catch it immediately in your editor, before you even run the code. Same thing applies when listening for events:


    socket.on("message", (data) => {
        console.log(data.text, data.sender);
    });

data here is automatically typed as { text: string; sender: string }, with full autocomplete, because of the ServerToClientEvents interface we defined earlier.

Step 7: A Note on Trust

It's worth being clear about one limitation here. These types only protect you at compile time, inside your own codebase. They do not validate data coming from a malicious or broken client at runtime. Someone could still send malformed data directly to your server using a raw WebSocket connection, bypassing TypeScript entirely. We will cover proper runtime validation later in Phase 5, but for now, these types are enough to keep your own client and server code consistent and error-free as you build.

Summary

  • We installed socket.io-client, which comes with its own TypeScript types out of the box
  • We created a shared socket.ts types file defining ServerToClientEvents, ClientToServerEvents, InterServerEvents, and SocketData
  • We applied these types to the server's Server instance, and to the client's Socket instance, in the correct generic order for each
  • Typed events give autocomplete and compile-time safety, but do not replace runtime validation of incoming data

This completes Phase 2. In the next post, we begin Phase 3 by actually connecting a client to the server, your first real-time "Hello World" using Socket.IO.

No comments:

Post a Comment

PHASE 4 — Topic 16: Broadcasting Messages to Everyone, or to Specific Users/Rooms

We've used several broadcasting patterns already across earlier posts. This post pulls everything together into one clear reference, and...