💎

Zodで条件分岐を含んだバリデーションを実装する

2024/03/05に公開

フォームの実装で「Aが〇〇のときだけBのバリデーションを変えたい」というケースはよくあります。

フィールドの値で分岐する場合はz.discriminatedUnion()を使用する

https://zod.dev/?id=discriminated-unions

フィールドの値によって、バリデーションを実施するかどうか決定する場合はz.discriminatedUnion() を使用することで実現できます。
v3.12.0でリリースされたメソッドで第1引数に指定されたキーを評価することでz.union()をよりもパース処理が高速になります。

const responseSchema = z.discriminatedUnion("status", [
  z.object({ status: z.literal("success"), data: z.string() }),
  z.object({ status: z.literal("failed"), error: z.instanceof(Error) }),
]);

export type Response = z.infer<typeof responseSchema>;

z.inferから推論される型は下記のようなユニオン型になっています。

type Response = {
    status: "success";
    data: string;
} | {
    status: "failed";
    error: Error;
}

特定の条件で分岐する場合はz.refine()を使用する

https://zod.dev/?id=refine
条件によってバリデーションルールが変化する場合はrefine() メソッド内で条件分岐によりバリデーションを行うことができます。例えば「日付の範囲選択バリデーション」をするスキーマを定義したい場合は下記のように定義できます。

export const formSchema = z
  .object({
    startDate: z.string().refine(
      (val) => {
        return val.length > 0;
      },
      { message: "日付を入力してください" },
    ),
    endDate: z.string().refine(
      (val) => {
        return val.length > 0;
      },
      { message: "日付を入力してください" },
    ),
  })
  .refine(
    ({ startDate, endDate }) => {
      const start = dayjs(startDate);
      const end = dayjs(endDate);
      return end.isAfter(start); // 終了日が開始日より未来かどうか
    },
    {
      message: "終了日は開始日より未来の日付にしてください",
      path: ["endDate"],
    },
  );

refine() はバリデーションを柔軟に行えますが、型推論が常にオプショナルになるのでdiscriminatedUnion() と比べると型の恩恵が減ってしまいます。またバリデーションロジックも複雑になりがちです。

個人的にはまず最初にdiscriminatedUnion()で実現できないか考えたのちにrefine()の使用を検討することをおすすめします。

frontend flat

Discussion