Install
npm install @tahanabavi/typesocket zod socket.io-clientzod and socket.io-client are peer dependencies — schemas only compare
correctly when every package shares one zod instance.
Quick start
1. Declare the contract. This file is imported by the frontend and the backend:
// ws-contracts.ts
import { z } from "zod";
import { defineSocketContracts } from "@tahanabavi/typesocket";
export const wsContracts = defineSocketContracts({
chat: {
sendMessage: {
direction: "client->server",
request: z.object({ roomId: z.string(), text: z.string().min(1) }),
ack: z.object({ id: z.string(), sentAt: z.number() }),
},
typing: {
direction: "client->server",
request: z.object({ roomId: z.string(), isTyping: z.boolean() }),
},
message: {
direction: "server->client",
payload: z.object({ id: z.string(), text: z.string(), user: z.string() }),
},
},
});2. Use it. The client is generated from the contract — there is no event name to mistype and no payload shape to keep in sync:
import { createSocketClient } from "@tahanabavi/typesocket";
import { wsContracts } from "./ws-contracts";
const client = createSocketClient({ url: "http://localhost:3001" }, wsContracts);
// server -> client: listening. `m` is fully typed.
const off = client.modules.chat.message.on((m) => console.log(m.user, m.text));
// client -> server with an ack: returns a Promise of the *validated* ack.
const { id } = await client.modules.chat.sendMessage({ roomId: "r1", text: "hi" });
// client -> server without an ack: fire-and-forget, returns void.
client.modules.chat.typing({ roomId: "r1", isTyping: true });
off(); // unsubscribeWhether an emit returns Promise<Ack> or void is decided by the contract:
declare ack and awaiting is meaningful, omit it and the return type is void.