はじめに
正直なところ、TypeScriptでバリデーション書くのって面倒だったんですよ。
APIから返ってくるデータ、フォームの入力値、環境変数。これらを信用していいのか常に不安で、かといって型だけじゃランタイムで何も検証してくれない。asでキャストして誤魔化す日々、誰しも経験あるんじゃないでしょうか。
そんな中で出会ったのがZod。GitHubで40,000スター以上、月間2億3,700万ダウンロード。もはやTypeScriptのバリデーションでは一択と言っていいレベルの定番ライブラリです。
30代になって思うのは、「型があるから大丈夫」は幻想だということ。ランタイムで検証しないと、本当の型安全性は得られない。
Zodとは
Zodは「TypeScript-first schema validation with static type inference」をキャッチコピーにした、スキーマ検証ライブラリです。
要するに「スキーマを定義すれば、そこから型が自動で推論される」という話。バリデーションロジックと型定義を二重管理する必要がなくなります。
現在の最新バージョンはZod 4で、MITライセンスのオープンソースとして公開されています。TypeScript v5.5以上が推奨されていますね。
特徴・メリット
1. ゼロ依存で超軽量
これ、意外と重要なんですけど、Zodは外部依存がゼロ。コアバンドルは約2KB(gzip圧縮)という軽さです。
バリデーションライブラリって重くなりがちですが、Zodはそこをしっかり抑えてる。コスパ的に、この軽さは正義です。
2. スキーマから型が自動生成される
個人的には、これが一番のメリットだと思います。
const UserSchema = z.object({
name: z.string(),
age: z.number(),
});
// 型が自動で推論される
type User = z.infer<typeof UserSchema>;
// => { name: string; age: number }
スキーマを変更すれば型も自動で変わる。型定義の二重管理から解放されるのは、QOL上がります。
3. イミュータブルなAPI設計
すべてのメソッドは新しいインスタンスを返す。元のスキーマを変更しないので、安心して使える。
const baseSchema = z.string();
const optionalSchema = baseSchema.optional(); // 元のbaseSchemaは変わらない
4. Node.jsでもブラウザでも動く
サーバーサイドでもクライアントサイドでも、同じスキーマが使える。これ地味に便利なんですよ。フォームのバリデーションをサーバーでも再利用できる。
5. エコシステムが充実
React Hook Form、tRPC、Prisma、Next.jsなど、主要なライブラリとの連携がしっかりしてる。導入のハードルが低い。
インストール方法
前提条件
TypeScript v5.5以上が推奨されています。
インストール
npm install zod
これだけ。追加の型定義パッケージとかは不要です。時短になる。
インポート
import { z } from "zod";
基本的な使い方
プリミティブ型のスキーマ
まずは基本から。
import { z } from "zod";
// 文字列
const stringSchema = z.string();
// 数値
const numberSchema = z.number();
// 真偽値
const booleanSchema = z.boolean();
// 日付
const dateSchema = z.date();
バリデーションの実行
バリデーションには2つの方法があります。
const schema = z.string();
// 1. parse - 失敗時は例外をスロー
try {
const result = schema.parse("hello");
console.log(result); // "hello"
} catch (error) {
console.error(error);
}
// 2. safeParse - 結果オブジェクトを返す(おすすめ)
const result = schema.safeParse("hello");
if (result.success) {
console.log(result.data); // "hello"
} else {
console.error(result.error);
}
個人的にはsafeParseの方が好みですね。try-catchを書かなくていいし、TypeScriptの型推論もきっちり効く。
オブジェクトスキーマ
実務で一番使うのはこれ。
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
age: z.number().min(0).max(120),
isActive: z.boolean().default(true),
});
type User = z.infer<typeof UserSchema>;
const result = UserSchema.safeParse({
id: 1,
name: "田中太郎",
email: "tanaka@example.com",
age: 35,
});
配列とオプショナル
const TagsSchema = z.array(z.string());
const ProfileSchema = z.object({
name: z.string(),
bio: z.string().optional(), // undefinedを許容
website: z.string().url().nullable(), // nullを許容
tags: z.array(z.string()).default([]), // デフォルト値
});
文字列の詳細なバリデーション
const schema = z.object({
email: z.string().email("有効なメールアドレスを入力してください"),
password: z.string()
.min(8, "パスワードは8文字以上で入力してください")
.max(100),
url: z.string().url(),
uuid: z.string().uuid(),
phone: z.string().regex(/^0\d{9,10}$/, "電話番号の形式が正しくありません"),
});
エラーメッセージをカスタマイズできるのが便利。
Union型とLiteral型
// Literal型
const statusSchema = z.literal("active");
// Union型(OR条件)
const resultSchema = z.union([
z.literal("success"),
z.literal("error"),
z.literal("pending"),
]);
// enumライクな使い方
const StatusEnum = z.enum(["active", "inactive", "pending"]);
type Status = z.infer<typeof StatusEnum>; // "active" | "inactive" | "pending"
実践的なユースケース
APIレスポンスの検証
外部APIのレスポンスを信用してはいけない。これ、30代エンジニアなら痛感してると思います。
const ApiResponseSchema = z.object({
status: z.number(),
data: z.object({
users: z.array(
z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
})
),
}),
meta: z.object({
total: z.number(),
page: z.number(),
perPage: z.number(),
}),
});
async function fetchUsers() {
const response = await fetch("/api/users");
const json = await response.json();
const result = ApiResponseSchema.safeParse(json);
if (!result.success) {
console.error("APIレスポンスが不正です:", result.error);
throw new Error("Invalid API response");
}
return result.data; // 型安全なデータ
}
フォームバリデーション
React Hook Formとの組み合わせが最強。
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const ContactFormSchema = z.object({
name: z.string().min(1, "お名前は必須です"),
email: z.string().email("有効なメールアドレスを入力してください"),
subject: z.string().min(1, "件名は必須です"),
message: z.string()
.min(10, "メッセージは10文字以上で入力してください")
.max(1000, "メッセージは1000文字以内で入力してください"),
});
type ContactForm = z.infer<typeof ContactFormSchema>;
function ContactForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<ContactForm>({
resolver: zodResolver(ContactFormSchema),
});
const onSubmit = (data: ContactForm) => {
// dataは型安全
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} />
{errors.name && <span>{errors.name.message}</span>}
{/* ... */}
</form>
);
}
環境変数の検証
これ、意外とやってない人多いんですよ。
const EnvSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
PORT: z.string().transform(Number).pipe(z.number().int().positive()),
});
const env = EnvSchema.parse(process.env);
// envは型安全
console.log(env.DATABASE_URL);
アプリ起動時に環境変数を検証しておけば、ランタイムで「環境変数がundefined」みたいなエラーで落ちることがなくなる。
スキーマの合成
const BaseUserSchema = z.object({
name: z.string(),
email: z.string().email(),
});
// 拡張
const AdminUserSchema = BaseUserSchema.extend({
role: z.literal("admin"),
permissions: z.array(z.string()),
});
// マージ
const WithTimestamps = z.object({
createdAt: z.date(),
updatedAt: z.date(),
});
const UserWithTimestampsSchema = BaseUserSchema.merge(WithTimestamps);
// Pick / Omit
const UserNameOnly = BaseUserSchema.pick({ name: true });
const UserWithoutEmail = BaseUserSchema.omit({ email: true });
TypeScriptのユーティリティ型と同じ感覚で使える。
カスタムバリデーション
const PasswordSchema = z.string()
.min(8, "8文字以上")
.refine(
(val) => /[A-Z]/.test(val),
"大文字を1文字以上含めてください"
)
.refine(
(val) => /[0-9]/.test(val),
"数字を1文字以上含めてください"
);
// 複数フィールドの関連チェック
const SignupSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).refine(
(data) => data.password === data.confirmPassword,
{
message: "パスワードが一致しません",
path: ["confirmPassword"],
}
);
refineで独自のバリデーションロジックを追加できる。
Zod 4の新機能
Zod 4が安定版としてリリースされました。主な改善点:
- パフォーマンス向上: パース速度が大幅に改善
- バンドルサイズ削減: さらに軽量化
- JSON Schema変換: ビルトインでサポート
- エラーメッセージの改善: より分かりやすいエラー表示
既存プロジェクトからの移行もスムーズにできるよう設計されています。
まとめ
Zodを導入して感じた変化:
- 型定義の二重管理: スキーマから自動生成されるので不要に
- バリデーションコード: 宣言的に書けて読みやすい
- バンドルサイズ: 2KBで超軽量
- 学習コスト: TypeScriptが分かれば即使える
- エコシステム: React Hook Form、tRPCとの連携が簡単
正直なところ、TypeScriptでバリデーションを書くなら、Zod一択ですね。
特に「型はあるけどランタイムで不安」という状況の人には刺さると思います。APIレスポンス、フォーム入力、環境変数。これらを型安全に扱えるようになると、開発の安心感が全然違う。
まだ試していない人は、まずAPIレスポンスの検証から始めてみてください。asでキャストしていたコードが、型安全なコードに生まれ変わりますよ。