Skip to content
TypeWire

@tahanabavi/typesocket

v2.2.0

Contract-driven, type-safe Socket.IO client — one Zod contract validated in both directions, with acks, middleware, queueing and a devtools instrumentation seam.

Install

bash
npm install @tahanabavi/typesocket zod socket.io-client

zod 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:

ts
// 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:

ts
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() // unsubscribe

Whether 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.