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> で型が自動生成されるので、型定義と実行時チェックを同時に管理できます!
実際にバリデーションをかけるときは parse か safeParse を使います。
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^)/