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 forClientToServerEvents— events the client sends, and the server listens forInterServerEvents— used only if you later run multiple server instances that talk to each otherSocketData— 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
pathoption 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.tstypes file definingServerToClientEvents,ClientToServerEvents,InterServerEvents, andSocketData - We applied these types to the server's
Serverinstance, and to the client'sSocketinstance, 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