Slide 1

Slide 1 text

HonoのRPCで 真の型安全が欲しかった kosei28

Slide 2

Slide 2 text

kosei28 ● 個人開発でWebやってます ● TypeScript大好き ○ フロントエンドもバックエンドも! ● (一応)Honoのコントリビューター 𝕏: @kosei_28

Slide 3

Slide 3 text

Honoとは ● JS/TSのWebフレームワーク ● 高速、軽量 ● あらゆるJavaScriptランタイムで動作する ○ エッジ環境でよく使われる ● RPCモード ○ サーバーの型をクライアントと共有して型安全に API呼び出しができる機能

Slide 4

Slide 4 text

RPCモードを使ってみる import { Hono } from "hono"; import { z } from "zod"; import { zValidator } from "@hono/zod-validator" ; const app = new Hono(); const routes = app.get( "/greeting" , zValidator ("query", z.object({ name: z.string() })), (c) => { const { name } = c.req.valid("query"); return c.json({ message: `Hello, ${name}!` }); } ); export type AppType = typeof routes; export default app; import { hc } from "hono/client" ; import type { AppType } from "./server" ; const client = hc("/"); const res = await client.greeting.$get({ query: { name: "kosei28" }, }); const data = await res.json(); // { message: string; } console.log(data.message); // “Hello, kosei28!” server.ts client.ts

Slide 5

Slide 5 text

実は完璧な型安全ではない😭

Slide 6

Slide 6 text

Middlewareで返したResponseには型がつかない const error = true; app.use(async (c, next) => { if (error) { return c.json({ error: "Internal Server Error" }, 500); } await next(); }); server.ts client.ts const res = await client.greeting.$get({ query: { name: "kosei28" }, }); const data = await res.json(); // { message: string; } console.log(data.message); // undefined console.log(data.error); // “Internal Server Error”

Slide 7

Slide 7 text

● Middlewareで極力Responseを返さない ○ Middlewareの代わりに関数を用意して各ルートから呼び出す ○ Middlewareの恩恵をあまり受けられない ● ValidatorもMiddleware ○ Zod Validatorのバリデーションエラーによる Responseはどうにもできない ○ そもそもバリデーションエラーを発生させない ■ Validatorでのバリデーションは型チェックだけにする ■ リクエスト前にクライアントでもバリデーションする ● スキーマを別のモジュールに定義して、サーバー・クライアントで共有する 対策1: Middlewareで返すResponseをどうにかする

Slide 8

Slide 8 text

対策2: 各ルートのResponseは全て200番台で返す ● Response.okでResponseがMiddlewareによるものか判別できる ○ Middlewareでは200番台のResponseを返さない ● デメリット ○ 不適切なステータスコード? ■ GraphQLは全て200 ■ 割り切ってしまえるなら問題なし ○ 結局、MiddlewareのResponseの型はわからない ○ ステータスコードによる型の分岐が使えない

Slide 9

Slide 9 text

const routes = app.get( "/greeting" , zValidator ("query", z.object({ name: z.string() })), (c) => { const { name } = c.req.valid("query"); if (error) { return c.json({ success: false as const, error: "Internal Server Error" , }); } return c.json({ success: true as const, data: { message: `Hello, ${name}!` }, }); } ); res.okの場合は型安全 const res = await client.greeting .$get({ query: { name: "kosei28" }, }); if (res.ok) { // この中では型安全 const result = await res.json(); if (result.success) { console.log(result.data.message); } else { console.log(result.error); } } server.ts client.ts

Slide 10

Slide 10 text

まとめ ● HonoのRPCモードはとても便利だが真の型安全ではない ● 対策 ○ MiddlewareによるResponseを減らす ○ クライアントでもバリデーションすることが重要 ○ 各ルートのResponseを200番台で返せば部分的な型安全にできる ● 型があるからと言って安全ではない ○ TypeScriptはデータと全く異なる型をアサーションできてしまう ○ 気づかぬうちに大事故が起こるかも …