Next.jsのApp Routerには特定のルートにカスタムリクエストハンドラーを作成できるRoute Handlerがあります。
Route Handlerは非常に強力ですが、デフォルトではResponseの型情報を参照できないという小さな欠点があります。この記事では、Type Aliasを活用してレスポンスの型情報を推論する方法をご紹介します。
Next.jsのRoute Handlerについて
型推論のソリューションに入る前に、Route Handlerとは何か、そしてなぜそれほど有用なのかを簡単に見てみましょう。
Next.jsのRoute Handlerは、アプリケーションの特定のルートに対してAPIエンドポイントやカスタムサーバーサイドロジックを作成する方法を提供します。これらはWeb RequestとResponse APIを使用しており、他のサーバーサイドJavaScriptフレームワークを使用したことがある開発者にとって馴染みやすいものです。
以下はRoute Handlerの簡単な例です:
export async function GET(request: Request) { return new Response('Hello, Next.js!') }
このハンドラーは、GETリクエストに対して単純に "Hello, Next.js!" というメッセージで応答します。
型情報の課題
Route Handlerは強力ですが、デフォルトではResponseの型情報を簡単に参照できないという制限があります。これにより、型関連のエラーが発生する可能性があり、アプリケーション全体で型の安全性を確保することが難しくなります。
この課題に対して、ZodやValibotなどのスキーマバリデーションライブラリを使ってレスポンスのスキーマを定義する方法がありますが、今回は別のアプローチとして型推論を使用する方法を紹介します。
その後、型推論を使った場合の強みとスキーマバリデーションライブラリを使った場合の強みについて比較します。
Route Handlerの戻り値の型を推論するType Aliasの導入
Route Handlerの戻り値の型を推論するType Aliasは、以下のような定義になります。
import { NextResponse } from 'next/server' type InferNextResponseType<T> = T extends (...args: any[]) => Promise<NextResponse<infer U>> ? U : never
InferNextResponseType<typeof GET>
のようにすることで、GETハンドラーのレスポンス型を推論することができます。
import { NextResponse } from 'next/server' import type { InferNextResponseType } from '@/helpers/api' export async function GET() { return NextResponse.json({ message: 'Hello, World!' }) } type ResponseType = InferNextResponseType<typeof GET> //^? { message: string }
ここで何が起こっているか詳しく見てみましょう:
- 条件付き型を使用してRoute Handler関数からレスポンスの型を推論する
InferNextResponseType<T>
というType Aliaseを定義します。 - このType Aliasは、
T
がPromise<NextResponse<infer U>>
を返す関数を拡張しているかどうかをチェックします。ここでU
はレスポンス本体の推論された型です。 - 条件が真の場合、
U
(推論された型)を返し、そうでない場合はnever
を返します。 - その後、このType Aliasを
typeof GET
と共に使用して、GETハンドラーのレスポンス型を推論します。
SWRを使用したクライアントの実装例
このType Aliasを利用してクライアントの実装がどのようになるかを見てみましょう。
今回はNext.jsと組み合わせて使うことが比較的多いSWR
を使いますが、他のライブラリでもfetch
でも同様の方法で型推論を行うことができます。
型推論しない場合
Type Aliasを利用せず、型推論をしない場合、SWRを使った実装は以下のようになります。
import useSWR from 'swr' const fetcher = (url: string) => fetch(url).then(res => res.json()) export default function Page() { const { data, error } = useSWR('/api/hello', fetcher) if (error) return <div>読み込みに失敗しました</div> if (!data) return <div>読み込み中...</div> return <div>{data.message}</div> }
この場合、data
の型はany
なので、data.message
の型チェックや自動補完が得られません。
型推論した場合
では、InferNextResponseType
を使用して型を推論してみましょう。
import useSWR from 'swr' import type { InferNextResponseType } from '@/helper/api' import type { GET } from '@/app/api/hello/route' const fetcher = (url: string) => fetch(url).then(res => res.json()) export default function Page() { const { data, error } = useSWR<InferNextResponseType<typeof GET>>('/api/hello', fetcher) if (error) return <div>読み込みに失敗しました</div> if (!data) return <div>読み込み中...</div> return <div>{data.message}</div> }
この実装例では:
- APIルートファイルから
InferNextResponseType
型をインポートします。 - この型を
useSWR
で使用し、受け取ると予想されるデータの正確な形状を指定します。 - これにより、TypeScriptは
data.message
が文字列であることを知り、より良い型チェックと自動補完を提供します。
Stackblitzにて実際のコードを試せるようにしているので、よければどうぞ。
https://stackblitz.com/~/github.com/toyamarinyon/inference-route-handler-type
型推論アプローチとスキーマバリデーションライブラリの比較
では、スキーマバリデーションライブラリ(ZodやValibot等)を使用するアプローチと比較して、それぞれの強みを見ていきましょう。
型推論アプローチの強み
- 設定の簡素さ: 追加のライブラリが不要で、TypeScriptの機能のみを使用するため、プロジェクトの依存関係が少なくなります。
- パフォーマンス: 型チェックはコンパイル時にのみ行われるため、実行時のオーバーヘッドがありません。
- 統合の容易さ: 既存のTypeScriptプロジェクトに簡単に統合でき、学習コストも低めです。
- 柔軟性: TypeScriptの型システムの全機能を活用できるため、複雑な型も簡単に表現できます。
スキーマバリデーションライブラリの強み
- ランタイムバリデーション: データの型だけでなく、値の妥当性も実行時にチェックできます。これは特に外部APIからのデータを扱う場合に有用です。
- エラーハンドリング: バリデーションエラーの詳細な情報を提供し、エラーメッセージのカスタマイズも可能です。
- 型の自動生成: スキーマから型を自動生成できるため、型定義の二重管理を避けられます。
- ドキュメント生成: スキーマからAPIドキュメントを自動生成できるツールと連携しやすいです。
まとめ
Type Aliasを使用することで、APIレスポンスの型情報を簡単に取得し、クライアントサイドのコードでそれを活用することができます。
一方で、プロジェクトの要件によっては、ZodやValibotなどのスキーマライブラリを使用するアプローチも検討する価値があります。特に、厳密なランタイムバリデーションやAPIドキュメントの自動生成が必要な場合は、スキーマライブラリの使用が適している可能性があります。
どちらのアプローチを選択するにせよ、型安全性を重視することで、より堅牢で保守性の高いアプリケーションを構築することができます。