Closed4
ZodのdiscriminatedUnion と union

union
いくつかのスキーマを並べて、「いずれかにマッチすれば OK」というチェックを行うための関数
以下の例だと文字列でも数値でもバリデーションが通るようになる
const schema = z.union([
z.string(),
z.number(),
]);

discriminatedUnion
z.discriminatedUnion("キー", [ スキーマA, スキーマB, ... ]) のように書くと、指定した「キー」の値をディスクリミネータとして使い、キーの値に応じて排他的にスキーマを切り替えることができる
- メリット:
- 「どの分岐を使うべきか」をキーの値で自動的に判断するため、バリデーションエラーが分かりやすく、意図しないスキーマとの混同を防げる
- 「参加」「不参加」など明確に分けられる選択肢がある場合に非常に便利

ラジオボタンで条件分岐する場合
discriminatedUnion を使わない場合
例えば、会社行事に「参加」「不参加」のラジオボタンがあるフォームを考えます。参加の場合は「参加場所」の入力が必須で、不参加の場合は入力項目が不要とする。
- 素朴な書き方
import { z } from "zod";
const formSchema = z.object({
participation: z.enum(["participate", "notParticipate"]), // ラジオボタン
place: z.string().optional(), // 参加したら必須
}).refine((data) => {
// participate のときは place が必要
if (data.participation === "participate") {
return Boolean(data.place);
}
return true;
}, {
message: "参加の場合は場所が必須です",
path: ["place"],
});
スキーマとしてはキーマとしては単一の z.object となり、participation が "participate" か "notParticipate" かを見て追加のチェックを行っている。
条件分岐のパターンが増えると、refine の中身が長くなり、可読性が下がる。

discriminatedUnion を使った場合
import { z } from "zod";
const participateSchema = z.object({
participation: z.literal("participate"),
place: z.string().min(1, "参加場所は必須です"),
// 必要に応じて他のフィールドを追加
});
const notParticipateSchema = z.object({
participation: z.literal("notParticipate"),
// 不参加の場合は入力項目なし あるいは別のフィールドを追加可能
});
export const eventFormSchema = z.discriminatedUnion("participation", [
participateSchema,
notParticipateSchema,
]);
// 型定義は z.infer ですぐ取得
export type EventFormType = z.infer<typeof eventFormSchema>;
- ディスクリミネータ(participation)で排他的に選択
- participation: z.literal("participate")
こちらがマッチした場合、participateSchema が適用される - participation: z.literal("notParticipate")
こちらがマッチした場合、notParticipateSchema が適用される
こうすることで、「参加」か「不参加」かをディスクリミネータで自動判別して、必要な項目が自動的にチェックされる。
もしフォーム入力で participation: "participate" が選択されていれば participateSchema が動作し、place が必須となる。
逆に participation: "notParticipate" であれば participateSchema にマッチしないので、place のバリデーションは不要とみなされる。
このスクラップは2025/01/19にクローズされました