ZodでTypeScriptのランタイム型検証を行う方法

問題

TypeScriptの型はコンパイル時にのみ存在します。ランタイムでは完全に消えてしまいます。

type User = {
  id: number;
  name: string;
  email: string;
};

const res = await fetch("/api/user/1");
const user: User = await res.json(); // 型キャストだけで、実際の検証はありません

サーバーから{ id: "abc", name: null }のようなデータが返ってきても、そのまま通過してしまい、後でUIで問題が発生します。原因のデバッグが非常に困難になります。

解決方法

Zodを使えば、型定義とランタイム検証を一つのスキーマで同時に行えます。

npm install zod
import { z } from "zod";

// スキーマ定義 = 型定義 + ランタイム検証ルール
const UserSchema = z.object({
  id: z.number(),
  name: z.string().min(1),
  email: z.string().email(),
});

// スキーマから型を自動抽出
type User = z.infer<typeof UserSchema>;

// APIレスポンスの検証
const res = await fetch("/api/user/1");
const data = await res.json();
const user = UserSchema.parse(data); // 不正なデータならここで即エラー!

エラーをスローせずに処理したい場合はsafeParseを使います。

const result = UserSchema.safeParse(data);

if (!result.success) {
  console.error(result.error.flatten());
  // { fieldErrors: { email: ["Invalid email"] } }
  return;
}

// result.dataは検証済みのUser型
console.log(result.data.name);

フォームバリデーションにもそのまま使えます。

const SignupSchema = z.object({
  username: z.string().min(3, "3文字以上入力してください"),
  password: z.string().min(8, "8文字以上入力してください"),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "パスワードが一致しません",
  path: ["confirmPassword"],
});

ポイント

  • TypeScriptの型はランタイムで消えるため、外部データには必ずランタイム検証が必要です
  • z.inferでスキーマから型を抽出すれば、型定義の重複を解消できます
  • parseはエラーをスローし、safeParseはResult型を返します
  • APIレスポンス、フォーム入力、環境変数など、外部から入るデータに積極的に活用しましょう