socket.ioを型安全に使えるライブラリを考えている

 自分のためにつくっているが、まだいろいろ考えないといけないことがある。

ひとまず、どういうものなのかをここに記す。
 

まず、serverが受信、clientが送信とする。
基本的なsocket.ioのコードだと、client側は以下になる。

```ts
const socket = io("http://localhost:3000");
socket.emit("eventName", data);

```

  

emit時に、必ず存在するeventに、正しい型のdataを送りたい。
一応、socketの初期化時にname spaceごとに型を与えて推測するようにできる。

[https://socket.io/docs/v4/typescript/](https://socket.io/docs/v4/typescript/)  

  

個人的には、trpcやts-restのように、自動生成される型安全な送信用のクライアントがほしい、と考えた。

サーバー側の実装を考える。

```ts
export type ServerAppRoute = {
	data: z.ZodSchema;
};
export type ServerAppRouter = {
	[key: string]: ServerAppRoute;
};

export const serverRouter = {
	comment: {
		data: z.object({
			comment: z.string(),
		}),
	},
	hello: {
		data: z.object({
			w: z.string(),
		}),
	},
	hello2: {
		data: z.object({
			word: z.string(),
		}),
	},
} as const satisfies ServerAppRouter;

```

comment, hello, hello2がeventNameになる。
そして、dataという部分がzodSchemaで定義されている。

サーバー側の受信を基本通りに書くと、こうなる。

  

```ts
const io = new Server({
	cors: {
		origin: "*",
	},
});
io.on("connection", (socket) => {
	socket.on("comment", (data) => {});
	socket.on("hello", (data) => {});
	socket.on("hello2", (data) => {});
});
io.listen(3000);

```

このあたりを型安全かつ簡単にしたい。

clientから送信する部分を考える。

たとえば

```ts
const client = initClient(serverRouter, socket);
client.comment({ comment: "comment is hello" });
```

このようにclient.と押したときにcomment, hello, hello2がサジェストされ、comment({と書いたときにcomment: “”が類推される。そういうふうにつくりたい。

  

そのようなinitClientは以下のように実装する。

```ts
const getClientEmit = <T extends ServerAppRoute,>(
	socket: Socket,
	key: string,
) => {
	return (data: z.infer<T["data"]>) => {
		socket.emit("socket-io-rest-contract", key, data);
	};
};

export const initClient = <T extends ServerAppRouter>(
	contract: T,
	socket: Socket,
) =>
	pipe(
		contract,
		D.mapWithKey((key) => getClientEmit(socket, key.toString())),
	) as {
		[K in keyof T]: T[K]["data"] extends undefined
			? () => void
			: (data: z.infer<T[K]["data"]>) => void;
	};

```

  

initClientはcontract, socketの2つを引数として受け取る。
contractはServerAppRouter型のジェネリクスである。
その後、objectのkey(comment, hello, hello2)でループしていき、getClientEmit関数へ通す。
そうすると、client.のサジェストでcomment, hello, hello2が表示されるようになる。
本当は、ネストしている可能性もあるので(comment.c1, comment.c2のように）、RouteなのかRouterなのかで判断して、再度関数を呼んだほうが良いだろう。

getClientEmitによって、あるデータを受け取って、socket.emitでsocket-io-rest-contractというeventNameで送信する。普通はeventNameがkeyになると思うが、keyをユニオン型として処理したいので、第一引数をkey, 第二引数をdateとすることにした。

最終的に、いまは無理やりasで\[K in keyof T\]などをして型をつけているが、これは本来、getClientEmit側でどうにかなるはず。ちょっとTypeScript力が足りていないのでサボっている部分である。

次に、サーバーの受信側を見てみよう。

```ts
io.on("connection", (socket) => {
	socket.on("socket-io-rest-contract", (key, data) => {
		const route = zodEnumFromObjKeys(serverRouter).parse(key);
		match(route)
			.with("comment", (input) => {
				const props = serverRouter[input].data.parse(data);
				console.log(props.comment);
			})
			.with("hello", (input) => {
				const props = serverRouter[input].data.parse(data);
				console.log(props.w);
			})
			.with("hello2", (input) => {
				const props = serverRouter[input].data.parse(data);
				console.log(props.word);
			})
			.exhaustive();
	});
});

```

zodEnumFromObjKeys関数にserverRouterを渡すと、serverRouterのkeyのenumとなる。
（つまりcomment, hello, hello2）
このschemaによってkeyをparseし、routeの型を確定させる。
あとはts-patternのmatchによってkeyごとの処理を書く。
こうすることで、新しくルートをserverRouterに足したとしても処理の記述が漏れることはなくなる。

あんまりいけてないのが、serverRouter\[input\].data.parse(data)のように必ずパースをしないといけないところで、ここは要改善。
 

そういう感じで、socket.ioを型安全に使えるライブラリを考えている。


appRouterを作成し、それをループで処理して、あるzodSchemaのオブジェクトを引数とする関数を生やすことで、自作のrpc-clientがつくれる。

送信の処理部分をsocket.ioではなくaxiosなどでもつくれるだろう（ただ、それならtrpcとかts-restで良いよね、という話になるが……）。

axiosなどのときは、inputとreturnの2つの型を考える必要があるだろう。(socket.ioはcall\_backを考えない限り、送りっぱなしなので。またserver側からclient側へemitするときは、べつのrouterを用意する必要がある。結局、行うのはだいたい一緒のことだ）

つくっているとなかなか面白い。