Zod でAPIレスポンスのバリデーションを整えたらエラー周りがキレイになった話

外部APIのレスポンスを as でキャストしていたコードに Zod を導入したところ、実行時エラーが減り、型の信頼度がかなり上がりました。その手順と感想をまとめます。

🙌 結論から

API レスポンスの型を as でキャストしていたコードに Zod を導入したら、「型は正しいはずなのに実行時に undefined になった」という事故がかなり減りました!

Zod はスキーマを書くだけで TypeScript の型も自動で推論されるので、型定義を二重に書く必要がありません。

外部 API のレスポンスや localStorage のデータなど、「型は合っているつもりだったが実際には違った」という境界に使うと、かなり効果を感じられます。

💡 以前の問題

仕事で使っていた TypeScript プロジェクトでは、外部 API からデータを取得するときにこういうコードを書いていました。

const res = await fetch('/api/user')
const data = await res.json() as User

TypeScript 的にはエラーが出ませんが、as はあくまでコンパイラへの「信じてくれ」という命令です。

実際のレスポンスに name フィールドがなかった場合でも、TypeScript はそれを検知できません(´・ω・`)

あるとき、本番環境で API のレスポンス構造が変わったことに気づかず、data.profile.email を参照する箇所で Cannot read properties of undefined エラーが出たことがありました。

TypeScript を使っているのに型の恩恵を活かせていない、という状況…

👀 Zod でバリデーションを書く

Zod はスキーマ定義から TypeScript の型を自動で生成できるライブラリです。

npm install zod
import { z } from 'zod'

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  profile: z.object({
    bio: z.string().nullable(),
  }),
})

type User = z.infer<typeof UserSchema>

z.infer<typeof UserSchema> で型が自動生成されるので、型定義と実行時チェックを同時に管理できます!

実際にバリデーションをかけるときは parsesafeParse を使います。

const res = await fetch('/api/user')
const raw = await res.json()

const result = UserSchema.safeParse(raw)

if (!result.success) {
  console.error(result.error.format())
  throw new Error('APIレスポンスの形式が不正です')
}

const user = result.data // この時点で User 型が保証される

safeParse は例外を投げずに { success, data, error } を返してくれるので、エラーハンドリングがしやすいです(^^)

parse の方はバリデーション失敗時に例外を投げるので、try-catch で扱いたい場面に向いています。

✨ 導入してみた感想

道入して一番ありがたかったのは、APIの構造変更に即座に気づけるようになったことです。

以前は変更に気づかず UI が壊れてから発覚していたのが、リクエスト時点でバリデーションエラーとして検知できるようになりました。

.nullable().optional() を明示的に書く作業が、ドキュメントの代わりにもなっているので、後からコードを読む人にも伝わりやすくなりました🔥可読性が増したのはチームにとってもいいことですね^^

一点だけ注意が必要なのは、レスポンスの型が複雑な場合にスキーマの記述量がそれなりに増えることです。

特にネストが深い場合は .object() の中に .object() を入れる形になり、最初は少し読みにくさを感じるかも・_・;)

ただ、スキーマを細かく分割して z.object({ profile: ProfileSchema }) のように組み合わせると、見通しが良くなります!

👍 まとめ

Zod は 「TypeScript の型は合っているが、実行時に崩れる」という境界に使うと、けっこう便利です!s

外部 API のレスポンスはその典型例で、as でキャストするよりも safeParse で検証する方が、実際の安全性がかなり上がります。

型定義とスキーマを1か所で管理できるのも、コードの見通しを良くしてくれるので重宝できます。

まだ as を使ったキャストが多いコードがあれば、ぜひ境界部分から少しずつ Zod に置き換えてみてください(^o^)/