💎

具体例で学ぶZodの使い方

2022/11/29に公開
5

はじめに

基本的な使い方は公式ページがわかりやすいのでサラッと書いてます。
具体例は実際に Zod を使ってみて気になった部分を書きました。

基本的な使い方

単純な文字列スキーマの作成
import { z } from "zod";

// 文字列のスキーマを作成する
const mySchema = z.string();
// スキーマから型を生成する
type MySchema = z.infer<typeof mySchema>
/*
  生成される型の中身
  type MySchema = string
*/

const input: MySchema = 100; // TypeError (型 'number' を型 'string' に割り当てることはできません!)
const input: MySchema = "hello!"; // OK!

z.infer<typeof T> とすることでスキーマから型を生成できます。

zod は form と組み合わせて使われることが多く、その場合は以下のようなオブジェクトのスキーマを定義します。

オブジェクトスキーマの作成
import { z } from "zod";

// オブジェクトのスキーマを作成する
const user = z.object({
  name: z.string(),
  age: z.number(),
  email: z.string().email(),
});
type User = z.infer<typeof user>;
/*
  生成される型の中身
  type User = {
    name: string;
    age: number;
    email: string;
  }
*/

そのほかの基本的な使い方は公式ページを参照してください。

具体例を見てみよう

エラーメッセージをカスタムできる

ほとんどのバリデーションで以下のようにエラーメッセージをカスタムできます。

z.string().min(5, { message: "5文字以上である必要があります。" });

状態によってエラーメッセージを出し分けることもできます。

const name = z.string({
  required_error: "名前は必須です。",
  invalid_type_error: "名前は文字列である必要があります。",
});

正規表現を使うとき

例: 郵便番号のバリデーション
const POST_CODE = new RegExp("^[0-9]{3}-[0-9]{4}$");
const postCode = z.string().regex(POST_CODE,"半角数字、ハイフン付きで入力してください(例: 123-4567)")

regexを使えば正規表現にも対応できます。

入力する場合はバリデーションが必要だけど、空のままでもいい

例:fromのinput で、もし入力するなら 2 文字以上の文字列だけど入力せず空のままでもいいとき

const inputName = z.string().min(2).or(z.literal(""));
type InputName = z.infer<typeof inputName>;
/*
  生成される型の中身
  type InputName = string;
  正確にはこんな感じ
  type InputName = 2文字以上のstring | ""
*/
const inputName = z.string().min(2).optional();
type InputName = z.infer<typeof inputName>;
/*
  生成される型の中身
  type InputName = string | undefined;
  正確にはこんな感じ
  type InputName = 2文字以上のstring | undefined;
*/

オブジェクトの中のオブジェクトも定義できる

const userSchema = z.object({
  name: z.string(),
  blogs: z
    .object({
      title: z.string(),
      body: z.string(),
    })
    .array(),
});
type UserSchema = z.infer<typeof userSchema>;
/*
  生成される型の中身
  type UserSchema = {
    name: string;
    blogs: {
        title: string;
        body: string;
    }[];
}
*/

transform はオブジェクト自体も変えられる

例:フォームでは名字と名前を別々入力させるが、フルネームとしても持っておきたいとき。

const schema = z
  .object({
    lastName: z.string(),
    firstName: z.string(),
  })
  .transform((arg) => {
    return {
      ...arg,
      fullName: `${arg.lastName} ${arg.firstName}`,
    };
  });
type User = z.infer<typeof schema>;
/*
生成される型の中身
type User = {
  fullName: string; ←追加されてる
  lastName: string;
  firstName: string;
};
*/

ファイル(画像)のバリデーションもできる

https://zenn.dev/kaz_z/articles/zod-image-file

おわりに

今後も気になる点があれば追記していきたいと思います。

参考記事

https://github.com/colinhacks/zod

GitHubで編集を提案

Discussion

Yusuke InaiYusuke Inai

zodについて興味があったのですごい参考になりました。

kazuhokazuho

ありがとうございます!
そう言ってもらえて嬉しいです!

susiyakisusiyaki

上と似てますが .optional() だと 空の状態=""になるので min(2)に引っかかり送信できません。

optionalメソッドは空文字列とは関係なく、undefinedかどうかを評価するものなのでundefinedはパスします。

const schema = z.string().min(2).optional();
const result1 = schema.safeParse(undefined);
const result2 = schema.safeParse("");

console.log(result1.success); // true
console.log(result2.success); // false

おそらくinputのバリデーションを想定した記述だと思うので、以下の手順または初期値が空文字列であることからこのように記述したと思うのですが一応補足しておきます

  1. inputに何かを入力
  2. 消して空の状態にする

ベターな表現としては、こんな感じかと思います!

optionalでスキーマを定義すると、inputの入力を消してフォームが空になった際に空文字列がハンドラに渡ります。そのため、2文字以下は許容されないという制約に引っかかってしまい要件を満たすことができません。

kazuhokazuho

おそらくinputのバリデーションを想定した記述だと思うので、以下の手順または初期値が空文字列であることからこのように記述したと思うのですが一応補足しておきます

おっしゃる通りformのinputを想定して記述してましたが、読み返してみるとたしかに説明不足ですね。
ベターな表現をお借りして今夜修正しておきます。
補足していただきありがとうございます!

nap5nap5

具体例で学ぶZodの使い方

とタイトルに記載してありましたので、ワークアラウンドの一つとして、safeParseを利用したneverthrowライブラリのResult型バリデーションロジックを作ってみました。

デモコードです。
https://codesandbox.io/p/sandbox/billowing-cherry-2suhi4?welcome=true

const validateAgeByZod = (
  data: UserData,
  threshold: number = 19
): Result<UserData, CustomZodErrorData> => {
  const parsed = z
    .number()
    .min(threshold, `年齢が若すぎます あなたは${data?.age}`)
    .safeParse(data?.age);
  if (!parsed.success) {
    return new Err(parsed.error);
  }
  return new Ok(data);
};

簡単ですが、以上です。