Next.jsのRoute Handlerを型安全にするType Alias

Next.jsのApp Routerには特定のルートにカスタムリクエストハンドラーを作成できるRoute Handlerがあります。

nextjs.org

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 }

ここで何が起こっているか詳しく見てみましょう:

  1. 条件付き型を使用してRoute Handler関数からレスポンスの型を推論するInferNextResponseType<T>というType Aliaseを定義します。
  2. このType Aliasは、TPromise<NextResponse<infer U>>を返す関数を拡張しているかどうかをチェックします。ここでUはレスポンス本体の推論された型です。
  3. 条件が真の場合、U(推論された型)を返し、そうでない場合はneverを返します。
  4. その後、この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の型チェックや自動補完が得られません。

useSWR without Type Inference

型推論した場合

では、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>
}

この実装例では:

  1. APIルートファイルからInferNextResponseType型をインポートします。
  2. この型をuseSWRで使用し、受け取ると予想されるデータの正確な形状を指定します。
  3. これにより、TypeScriptはdata.messageが文字列であることを知り、より良い型チェックと自動補完を提供します。

useSWR with Type Inference

Stackblitzにて実際のコードを試せるようにしているので、よければどうぞ。

https://stackblitz.com/~/github.com/toyamarinyon/inference-route-handler-type

型推論アプローチとスキーマバリデーションライブラリの比較

では、スキーマバリデーションライブラリ(ZodやValibot等)を使用するアプローチと比較して、それぞれの強みを見ていきましょう。

型推論アプローチの強み

  1. 設定の簡素さ: 追加のライブラリが不要で、TypeScriptの機能のみを使用するため、プロジェクトの依存関係が少なくなります。
  2. パフォーマンス: 型チェックはコンパイル時にのみ行われるため、実行時のオーバーヘッドがありません。
  3. 統合の容易さ: 既存のTypeScriptプロジェクトに簡単に統合でき、学習コストも低めです。
  4. 柔軟性: TypeScriptの型システムの全機能を活用できるため、複雑な型も簡単に表現できます。

スキーマバリデーションライブラリの強み

  1. ランタイムバリデーション: データの型だけでなく、値の妥当性も実行時にチェックできます。これは特に外部APIからのデータを扱う場合に有用です。
  2. エラーハンドリング: バリデーションエラーの詳細な情報を提供し、エラーメッセージのカスタマイズも可能です。
  3. 型の自動生成: スキーマから型を自動生成できるため、型定義の二重管理を避けられます。
  4. ドキュメント生成: スキーマからAPIドキュメントを自動生成できるツールと連携しやすいです。

まとめ

Type Aliasを使用することで、APIレスポンスの型情報を簡単に取得し、クライアントサイドのコードでそれを活用することができます。

一方で、プロジェクトの要件によっては、ZodやValibotなどのスキーマライブラリを使用するアプローチも検討する価値があります。特に、厳密なランタイムバリデーションやAPIドキュメントの自動生成が必要な場合は、スキーマライブラリの使用が適している可能性があります。

どちらのアプローチを選択するにせよ、型安全性を重視することで、より堅牢で保守性の高いアプリケーションを構築することができます。