Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Typescript型推論の限界

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for nikawa2161 nikawa2161
August 06, 2025
30

 Typescript型推論の限界

Avatar for nikawa2161

nikawa2161

August 06, 2025
Tweet

More Decks by nikawa2161

Transcript

  1. 背景:型推論が失われる瞬間 // サーバー側 const app = new Hono() .get("/users", (c)

    => c.json({ users: [] })) .onError((err, c) => { return c.json({ error: err.message }, 500); // この型が hc 側へ伝播しない }); export type AppType = typeof app; // クライアント側 const client = hc<AppType>("/api"); const res = await client.users.$get(); // エラーのステータスが分からない 3
  2. 試行錯誤の軌跡 アプローチ 1: onError での例外キャッチ 狙い: カスタムクラスを throw し、onError で一括キャッチ

    一括で管理できるかつ、シンプルなのでこちらでできるならベスト 例外 → ステータス変換 app.onError((err, c) => { if (err instanceof BadRequestError) { return c.json({ error: err.message }, 400); } return c.json({ error: "Internal error" }, 500); }); 4
  3. アプローチ 1 の失敗理由 失敗理由: onError の返却型が hc に届かない middleware のレスポンスの型がサポートされていない?

    (GitHub Issue #2719) compose() で例外時に onError に投げる仕様のため、推論できない 5
  4. アプローチ 2: カスタムエラークラス 3 パターン パターン 1: 親クラス + ステータス込み

    class CustomError extends Error { constructor(message: string, public status: StatusCode = 500) { super(message); } } export class BadRequestError extends CustomError { constructor(message: string, public status: 400 = 400) { super(message); } } 6
  5. パターン 2: 型マッチでステータス指定 export class BadRequestError extends Error { constructor(message:

    string, public status: 400 = 400) { super(message); } } export function handleError(c: Context, err: unknown) { if (err instanceof BadRequestError) { return c.json({ error: err.message }, err.status); } } 7
  6. パターン 3: 純粋クラス + パターンマッチ export class BadRequestError extends Error

    { constructor(message: string) { super(message); } } export function handleError(c: Context, err: unknown) { if (err instanceof BadRequestError) { return c.json({ error: err.message }, 400); // リテラル指定 } } 結果: 同じ理由で失敗 8
  7. なぜ全て失敗したのか? Hono の型推論の仕様制約 1. ハンドラ内直接記述が必須 // 推論される app.get("/users", (c) =>

    { return c.json({ error: "Bad Request" }, 400); }); // 推論されない app.get("/users", (c) => { return handleError(c, error); // 関数経由 }); 2. モジュールスコープの制約 ハンドラー外で定義された関数の戻り値は推論されない 9
  8. 全アプローチ共通の失敗理由 ルートハンドラ内で直接記載されていない: Hono はハンドラ内で直接記述された c.json<Body, Status> のみ推論する その Status がリテラル(

    400 , 404 ...)である必要 handleError がモジュール外にある: 外部関数経由では推論が働かない 結果: const res = await client.$get(); ではエラーのステータスは分からない 10
  9. 主なボトルネック 1. onError の型推論限界 これは throw された場合にハンドラー外で実行されるため hono の仕様外 app.onError((err,

    c) => { return c.json({ error: "Something went wrong" }, 500); // ← この型が hc 側へ伝播しない }); 11
  10. 2. モジュールスコープの制約 ハンドラ内に直接記述しないと型推論ができない仕様を突破できない // 外部関数では型推論されない export function handleError(c: Context, err:

    unknown) { if (err instanceof BadRequestError) { return c.json({ error: err.message }, 400); // この型が伝播しない } } // ハンドラ内直接記述のみ推論される app.get("/users", (c) => { return c.json({ error: "Not found" }, 404); // この型は推論される }); 12
  11. 現在の落とし所 カスタムエラーで投げたものを onError 内でパターンマッチして一括管理 app.onError((err, c) => { if (err

    instanceof BadRequestError) { return c.json({ error: err.message }, 400); } if (err instanceof NotFoundError) { return c.json({ error: err.message }, 404); } if (err instanceof UnauthorizedError) { return c.json({ error: err.message }, 401); } // デフォルトエラー return c.json({ error: "Internal Server Error" }, 500); }); 13
  12. 達成できたこと vs 残る課題 達成できたこと: エラーハンドリングの共通化 課題だった型の伝播は達成できず: const res = await

    client.$get() でエラーのステータスは型推論されない クライアント側でのステータス推論は別途検討が必要 14
  13. 参考資料 Hono RPC Guide Elegant Error Handling with Hono RPC

    (2024) GitHub Issue #2719 - middleware 型サポート GitHub Issue #3485 - RPC client 型推論問題 21