😱

TypeScriptのゾッとする話 ~ Zodの紹介 ~

commits47 min read

現在( 2021/09/13 )、この記事の情報は古くなっている可能性があります!
そのため、なるべくは公式ドキュメントを参照してください。
参照: 公式ドキュメント

この記事の冒頭と末尾には茶番が含まれます!予めご留意ください🙇‍♂️🙇‍♀️

この記事について

先日、「 エンジニアとして一番怖いモノは何? 」と知人に尋ねると、

実は、TypeScriptが一番怖い

と言ってきました。
そんな訳ないだろうと思っていたのですが、どうやら知人は本気の様子。

「 TypeScriptが嫌いならまだしも、TypeScriptが怖いとは、これはナニかあるな🤔 」

と思った私は、TypeScript Firstなライブラリである Zod を知人に紹介して、事の真意を確かめようとしたのでした。 怖いならもっと怖がらせてやろうと思ったのは内緒🤫

しかし、知人に紹介するだけでは勿体ないので、今回は皆さんに知見を交えながら Zod の事を紹介していこうと思います💪

注意として、この記事は以下のリポジトリのREADME.mdを参考にしています。

https://github.com/colinhacks/zod

そのため、記事内のソースコードにはREADME.md内のソースコードを引用している部分がありますので、ご理解いただけますと幸いです🙏

では早速、解説してきましょー👨‍🚀👩‍🚀

Zodについて

https://github.com/colinhacks/zod

Zodとは、TypeScript Firstなバリデーションライブラリで、Blitzにも使われている今Hotなライブラリです。

特徴としては、以下のようなものがあります👇

特徴

  • 依存関係ゼロ
  • ブラウザとNode.jsで動作します( denoにも対応🦕 )
  • サイズが小さい: 8kb minified + zipped
  • イミュータブル
  • 簡潔で連鎖可能なインターフェース
  • Functional approach: parse, don't validate
  • プレーンJavaScriptでも動作します!TypeScriptを使用しなくても大丈夫!

基本的な使い方

先ずは基本的な使い方から見て行きましょう👀

スキーマを作って検証する例
import * as z from "zod";

// スキーマを作成
const schema = z.object({
  str: z.string(),
})

// 値を検証する
try {

  const ok = schema.parse({ str: "" });
  const throw_error = schema.parse({ str: 0 });

} catch(err) { // 検証に失敗するとエラーが投げられます。

  console.error(err); 
  /*
    // エラーオブジェクトの内容👇
    [
      {
        "code": "invalid_type", // エラータイプ
        "expected": "string",   // 期待した型
        "received": "number",   // 受け取った値の型
        "path": [ "str" ],      // エラーが発生したプロパティへのパス
        "message": "Expected string, received number" // エラー内容
      }
    ]  
  */

}

上記のコードでは、{ str: string }の値を期待するschemaを定義し、そのschemaが持つparse()を実行する事で、引数に渡された値が期待する型かどうかを検証しています。

この時、schema.parse()の動作は以下のようになります。

  • ✅検証成功時 -> 期待した型の値を返します
  • ❌検証失敗時 -> 検証エラーの内容を持ったZodErrorオブジェクトを投げます

.parse()は、検証失敗時にはErrorオブジェクトを投げるため try...catch文 を使う必要がありますが、Errorを投げない検証方法もありますので、状況によって使い分けることが可能です。

また注意点としては、検証値がプリミティブ型ではない時、検証結果は引数の値とは違う値になります。そのため、以下の処理は常にfalseを返すことになります👇

schemaは上記のモノを使用
const value = { str: "test" }
const value2 = schema.parse(value) // 検証が成功し、結果をsameValueに入れる

// 引数の値を返すわけではありませんので、以下の処理はfalseになります🙅‍♂️🙅‍♀️
console.log( value === value2 ); // ==> false

上記の挙動が特に問題となる事は少ないと思いますが、知見として共有しておきます🦴

型の生成

次に最大の特徴である、生成したスキーマからTypeScriptの型を生成する機能を紹介したいと思います🐰

スキーマから型を生成するサンプル
import * as z from "zod";

const schema = z.object({
  str: z.string(),
  num: z.number()
})

// スキーマから型を生成!
type SchemaType = z.infer<typeof schema>;
/*
  {
    str: string
    num: string
  }
*/

上記のソースコード中にあるz.infer<>の型引数に、スキーマの型( typeofで変換した型 )を渡すことで、スキーマが期待している値の型を生成することが出来ます。

この機能が凄い所は、今まで型定義をしてから実装する所を、ほとんど実装のみの記述で済む点です。これによって、型定義と実装との齟齬を無くす事が出来るうえに記述量を減らすことが出来ます。

勿論、型生成できるモノはstring型やnumber型だけでなく他の型も生成可能です。
それらの型の詳細は後述しますが、ほとんどの型を生成できるので、相当特殊な要件ではない限り型を生成できると思います。

まとめ

さて、これで基本的な使い方の解説は終了です。

少し短い解説に思えるかもしれませんが、基本的にはスキーマ作って検証するだけなので、上記の使い方を覚えておけばZodを扱えるようになります。何なら、VSCodeの入力補完を活用すれば、ドキュメントを読まずとも書けてしまうくらいです。そのくらいシンプルなAPI設計もZodの魅力ですね✨

次は、スキーマを定義するための関数(以後、ZodType)について解説していきます🌭

String型

String型のスキーマを定義するにはz.string()を使って定義します。

String型のスキーマには様々なオプションが用意されているので、そのオプションを使う事で柔軟な定義が可能となっています👇

様々なString型スキーマを定義
import * as z from "zod";

z.string();              // 単純な文字列
z.string().min(5);       // 5文字以上の文字列
z.string().max(5);       // 5文字以下の文字列
z.string().length(5);    // 固定幅の文字列
z.string().email();      // メールアドレス文字列
z.string().url();        // URL文字列
z.string().uuid();       // UUID文字列
z.string().regex(regex); // 正規表現にマッチする文字列
z.string().nonempty();   // 空文字列以外の文字列

上記の email・url・uuid などのオプションは、validator.jsを用いて検証しますので、扱う際はvalidator.jsを参照してください。

Number型

Number型のスキーマを定義するにはz.number()を使って定義します。

Number型にも以下の便利なオプションが用意されています👇

様々なNumber型スキーマを定義
import * as z from "zod";

z.number();               // 単純な数値( NaNとBigInt型は含まない )
z.number().min(5);        // 5以上の数値( >= 5 )
z.number().max(5);        // 5以下の数値( <= 5 )
z.number().int();         // 整数型の数値
z.number().positive();    // 0よりも大きい数値( > 0 )
z.number().nonnegative(); // 0以上の数値( >= 0 )
z.number().negative();    // 0より小さい数値( < 0 )
z.number().nonpositive(); // 0以下の数値( <= 0 )

注意点としては、NaNとBigInt型を含まない事ぐらいでしょうか。

BigInt型は別のZodTypeとして用意されているので大丈夫かと思いますが、NaNについてはZodTypeが用意されていないので、自作する必要があります👇

NaNを検知するスキーマを自作する
// NaNを検知するスキーマ( ZodSchemaを用いてnumber型にする )
const NaNSchema: z.ZodSchema<number> = z.any().refine(Number.isNaN)

// NaNを許容する数値スキーマ
const NumOrNaNSchema = z.number().or(NaNSchema)

// 型生成( NaNSchemaをZodSchemaでnumber型にしてないとanyになるので注意! )
type SchemaType = z.infer<typeof NumOrNaNSchema>;
// number

NaNを許容する時なんてあるのかは甚だ疑問ですが、IT業界は稀がよくある業界なので一応共有しておきます🎫

NaNの判定には色々と罠がありますので、注意してください!

Boolean型

Boolean型のスキーマを定義するにはz.boolean()を使って定義します。

Boolean型には固有のオプションなどは無いので、現状では簡単なスキーマを定義するのみとなっています👇

Boolean型のスキーマを定義
import * as z from "zod";

z.boolean(); // 単純なBoolean型のスキーマ

BigInt型

BigInt型スキーマを定義するにはz.bigint()を使って定義します。
BigIntについては、以下のMDNのドキュメントを参照してください。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/BigInt

また、このZodTypeにはNumber型のような特別なオプションはありません。
そのため、出来ることは単純な型スキーマを定義する事のみとなっています👇

BigInt型のスキーマを定義
import * as z from "zod";

z.bigint(); // 単純なBigInt型のスキーマ

BigInt型は、最近?できた型なので使う際には注意してください!
特にJSON文字列に変換する際は、toJSON()自前で実装する必要があります( 2021/05時点 )。

Date型

Date型スキーマを定義するにはz.date()を使って定義します。

Date型には固有のオプションなどは無いので、現状では簡単なスキーマを定義するのみとなっています👇

Date型のスキーマを定義
import * as z from "zod";

z.date(); // 単純なDate型のスキーマ

Undefined型

undefinedのスキーマを定義するにはz.undefined()を使って定義します。

undefinedのスキーマを定義
import * as z from "zod";

z.undefined(); // undefined

またoptional() オプションを用いる事で、他のZodTypeに付随させることが可能です。

String型のスキーマをoptionalにする
z.string().optional(); // string | undefined

Null型

nullのスキーマを定義するにはz.null()を使って定義します。

nullのスキーマを定義
import * as z from "zod";

z.null(); // null

またnullable()オプションを用いる事で、他のZodTypeに付随させることが可能です。

String型のスキーマをnullableにする
z.string().nullable(); // string | null

Void型

void型は、null | undefinedとなるような型となっています。
void型のスキーマを作成するにはz.void()を使って定義します。

void型のスキーマを定義
import * as z from "zod";

z.void(); // null | undefined

基本的にFunction型を定義する際に使用するので、単体での使用頻度は少ないかと思います。

Any型

anyのスキーマを作成するにはz.any()を使って定義します。

anyのスキーマを定義
import * as z from "zod";

z.any();   // any

z.any().parse("any");   // ✅ どの値でも検証に成功します

Never型

neverのスキーマを作成するにはz.never()を使って定義します。

neverのスキーマを定義
import * as z from "zod";

z.never(); // never

z.never().parse("any"); // ❌ どの値も検証に失敗します

Literal型

Literal型のスキーマを作成するにはz.literal()を使って定義しますが、実行する際は引数に検証したいをLiteral型の値を渡す必要があります👇

Literal型のスキーマを定義
import * as z from "zod";

z.literal(1);         // 1 のスキーマ
z.literal("hoge");    // "hoge" のスキーマ
z.literal(10n);       // 10n のスキーマ( 10nはBigInt型のこと )
z.literal(true);      // true のスキーマ
z.literal(undefined); // undefined のスキーマ
z.literal(null);      // null のスキーマ

注意点として、引数に渡せる値は string | number | bigint | boolean | null | undefined のみとなっています。

Array型

Array型のスキーマを作成するにはz.array()を使って定義しますが、実行する際は引数にZodTypeを渡す必要があります。

Array型にも便利なオプションがあります👇

import * as z from "zod";

z.array(z.string());            // string[]
z.array(z.string()).min(5);     // 5以上の要素を持つstring[]
z.array(z.string()).max(5);     // 5以下の要素を持つstring[]
z.array(z.string()).length(5);  // 5要素を持つstring[]
z.array(z.string()).nonempty(); // 空配列でないstring[]

また、array()オプションを用いる事で、他のZodTypeに付随させることが可能です。

z.string().array();            // string[]
z.string().optional().array(); // (string | undefined)[]
z.string().array().optional(); // string[] | undefined

注意点として、.array()オプションを使った場合は実行順で期待する型が変わります。
そのため、他のオプションが絡むようならz.array()を使った方が良いと思います。

Object型

Object型のスキーマを作成するにはz.object()を使って定義しますが、実行する際は引数にZodTypeを含んだオブジェクトを渡す必要があります👇

import * as z from "zod";

// { name: string; age:number; } のスキーマ
z.object({      
  name: z.string(),
  age: z.number()
})

そして、このObject型のZodTypeには、他のZodTypeとは少し違ったオプションが沢山ありますので、それらを一つ一つ解説したいと思います。

.shape

.shapeを用いる事で、プロパティに設定したスキーマを取得することが出来ます。

.shapeの挙動の確認
const schema = z.object({ str: z.string(), num: z.number() });

const str = schema.shape.str; // stringのスキーマ

const num = schema.shape.num; // numberのスキーマ

使いどころ

このオプションにより、Object内部にスキーマをまとめ易くなります。
以下に例を示します。

shapeの活用例
const UserSchema = z.object({
  name: z.string().max(50),
  age: z.number().max(100),
});

const UserName = UserSchema.shape.name;
const UserAge = UserSchema.shape.age;

上記のようにすることで、UserSchemaを変更するだけでUserNameUserAge両方のスキーマを変更することが出来ます。これによって、わざわざUserNameUserAgeを管理せずとも、UserSchemaとの整合性を高めることが出来るので非常に便利なオプションとなっています。

.extend()

.extend()オプションを使う事で、Objectスキーマのプロパティを上書きしながら、新しいObjectスキーマを作成することが出来ます。

.extend()の挙動の確認
const UserSchema = z.object({
  id: z.number(),
  name: z.string().max(50),
  age: z.number().max(100),
});

// UserSchemaから管理者のスキーマを作成する
const AdminSchema = UserSchema.extend({
  id: z.string(),       // 既存のプロパティを上書き
  isAdmin: z.boolean(), // 新規プロパティを追加
});

type UserType = z.infer<typeof UserSchema>;
/*
  {
    id: number;
    name: string;
    age: number;
  }
*/

type AdminType = z.infer<typeof AdminSchema>;
/*
  {
    id: string;       // number => stringに変化してる
    name: string;
    age: number;
    isAdmin: boolean; // 新しく追加されている
  }
*/

注意点としては、プロパティが被っている場合は上書きされてしまいますので、間違って上書きしてしまわないよう注意しましょう。また、引数には.shapeオプションの値も渡せますので、以下の方法でも書くことが可能です。

.shapeを使ってextendする
const HelloSchema = AnySchema.extend( WorldSchema.shape );

使いどころ

このオプションは、上記の例でも分かる通り、似たような構造を持ったスキーマを作る際に大変便利です。createdAtupdatedAtなどのテーブルが共通して持っている要素などは、このオプションで定義する事をオススメします。

共通の構造に.extend()を使う例
const CommonSchema = z.object({
  createdAt: z.date(),
  updatedAt: z.date(),
  deletedAt: z.date().optional(), 
})

const UserSchema = CommonSchema.extend({/* -- 省略 -- */});
const TaskSchema = CommonSchema.extend({/* -- 省略 -- */});

.merge()

.merge()オプションを使う事で、二つのObjectスキーマから一つのObjectスキーマを作成することが出来ます。

.merge()の挙動の確認
const HasIdSchema = z.object({ id: z.number() });
const BaseUserSchema = z.object({ id: z.string(), name: z.string() });

// HasIdSchemaとBaseUserSchemaからスキーマを作成
const UserSchema = BaseUserSchema.extend(HasIdSchema);

type UserType = z.infer<typeof UserSchema>;
/*
  {
    id: number;   // プロパティは引数に渡された方が優先されます
    name: string;
  }
*/

注意点として、引数にはObjectスキーマしか渡せません。また被っているプロパティがあった場合は、引数に渡されたObjectスキーマの方が優先されます。

使いどころ

.extend()オプションと似ていますが、引数に渡されたObjectスキーマのunknownKeysポリシー( strip/strict/passthrough )を引き継ぐ点が違います。

.strict()オプションを用いた場合を見てみます👇

.merge()の活用例
const A = z.object({ str: z.string() });

// 検証時に余計なプロパティがあった場合は、エラーを投げるようにする
const B = z.object({ num: z.number() }).strict();

// 要らない値を含んだオブジェクト
const value = { str: "", num: 0, hoge: "要らない値" };

A.extend( B.shape ).parse( value ); // ✅検証が通る
A.merge( B ).parse( value );        // ❌.strict()によってエラーになる

上記の挙動より、スキーマの条件を重要視している所では.merge()を使い、そうで無い所では.extend()を使うといった使い分けをしたらいいと思います。

筆者的には.extend()でスキーマを作って、そのスキーマにunknownKeysポリシーを設定する方が分かりやすいと思っています。実は.merge()使ったことないなんて言えn...

.pick()

.pick()オプションを使う事で、Objectスキーマから特定のプロパティのみを取ってきて、一つのObjectスキーマを作成することが出来ます。

.pick()の挙動の確認
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  task: z.object({
    title: z.string()
  })
});

// idとnameだけを取ってきてObjectスキーマを作る
const BasicUserSchema = UserSchema.pick({ id: true, name: true })

type BasicUserType = z.infer<typeof BasicUserSchema>;
/*
  {
    id: number;
    name: string;
  }
*/

使いどころ

reduxなどのデータが一枚岩となっているような場合は、このオプションを活用する事で柔軟にスキーマを定義できます。

具体的な所で言うと、フォームの状態管理では一枚岩のようなデータ構造になっていることが多いので、そのような場合に使いやすいと思います👇

const FormValuesSchemas = z.object({
  name: z.string(),
  age: z.nubmer(),
  tasks: z.string().array(),
});

// ユーザー情報を扱うスキーマ
const UserSchema = RootStoreSchemas.pick({ name: true, age: true });

// タスク情報を扱うスキーマ
const TaskSchema = RootStoreSchemas.pick({ tasks: true });

.omit()

.omit()オプションを使う事で、Objectスキーマから特定のプロパティを除外して、一つのObjectスキーマを作成することが出来ます。

.omit()の挙動の確認
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  tasks: z.string().array(),
});

// tasksを除外してObjectスキーマを作る
const BasicUserSchema = UserSchema.omit({ tasks: true })

type BasicUserType = z.infer<typeof BasicUserSchema>;
/*
  {
    id: number;
    name: string;
  }
*/

使いどころ

ほとんど.pick()と同じです。使いやすい方を使えばいいと思います👇

.omit()の活用例
const FormValuesSchemas = z.object({
  name: z.string(),
  age: z.nubmer(),
  tasks: z.string().array(),
});

// ユーザー情報を扱うスキーマ
const UserSchema = RootStoreSchemas.omit({ tasks: true }); 

// タスク情報を扱うスキーマ
const TaskSchema = RootStoreSchemas.omit({ name: true, age: true });

.partial()

.partial()オプションを使う事で、Objectスキーマ内のプロパティ全てをオプショナルに出来ます。

.partial()の挙動の確認
const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
  location: z.object({
    lat: z.number(),
    long: z.number(),
  })
})

// UserSchemaをオプショナルにしたスキーマ
const PartialUserSchema = UserSchema.partial();

type PartialUserType = z.infer<typeof PartialUserSchema>;
/*
  {
    name?: string | undefined;
    age?: number | undefined;
    location?: { lat: number; long: number } | undefined;
  }
*/

このオプションは、1レベルの深さまでしかオプショナルにしません。
深いレベルまでオプショナルにしたい場合は、.deepPartial()オプションを使用して下さい。

使いどころ

具体的な例で言うと、更新APIに送信する値を検証する時などに大変便利です👇

.partial()の活用例
// ユーザー情報を更新する関数
// PartialUserSchemaなどは上記で定義したモノ
const updateUser = async (values: PartialUserType) => {
  const updateValues = PartialUserSchema.parse(values); // 値を検証する

  /* -- ここで更新処理をする -- */
}

.deepPartial()

.deepPartial()オプションを使う事で、Objectスキーマ内のプロパティの深い所までをオプショナルに出来ます。

.deepPartial()の挙動の確認
const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
  location: z.object({
    lat: z.number(),
    long: z.number(),
  })
})

// プロパティの深い所までオプショナルにしたスキーマ
const DeepPartialUserSchema = UserSchema.deepPartial();

type  DeepPartialUserType = z.infer<typeof DeepPartialUserSchema>;
/*
  {
    name?: string | undefined;
    age?: number | undefined;
    location?: { 
      lat?: number | undefined; 
      long?: number | undefined;
    } | undefined;
  }
*/

使いどころ

基本的に.partial()オプションと同じだと思います。

.passthrough()

.passthrough()オプションを使う事で、検証結果に無関係なプロパティを含めることが出来ます。

言葉だけだと分かりづらいと思いますので、以下の例で挙動を確認してください👇

.passthrough()の挙動の確認
const UserSchema = z.object({ name: z.string() });

// 関係ない値`age`を含んだ入力値
const values = { name: "", age: 0 };

// デフォルトの挙動
const defaultParse = UserSchema.parse(values);
console.log(defaultParse); // { name: "" }

// .passthrough()の挙動
const passParse = UserSchema.passthrough().parse(values);
console.log(passParse); // { name: "", age: 0 }

.strict()

.strict()オプションを使う事で、検証時に無関係なプロパティがあった場合に検証失敗するように出来ます。

言葉だけだと分かりづらいと思いますので、以下の例で挙動を確認してください👇

.strict()の挙動の確認
const UserSchema = z.object({ name: z.string() });

const values = { name: "", age: 0 };

// デフォルトの挙動
UserSchema.parse(values); // ✅ 検証に成功します

// .strict()の場合
UserSchema.strict().parse(values); // ❌ 検証に失敗します

// 成功させるには無関係な値を除外する必要があります
const { age, ...userValues } = values; // 無関係な値を除外する
UserSchema.strict().parse(userValues); // ✅ 検証に成功します

.strip()

.strip()オプションを使う事で、検証時の挙動をデフォルトに戻すことが出来ます👇

.strip()の挙動の確認
const StrictUserSchema = z.object({ name: z.string() }).strict();

const values = { name: "", age: 0 };

StrictUserSchema.parse(values);         // ❌ 検証に失敗します
StrictUserSchema.strip().parse(values); // ✅ 検証に成功します

.catchall()

.catchall()オプションを使う事で、検証時の無関係なプロパティを検証することが出来ます👇

.catchall()の挙動の確認
const UserSchema = z.object({ name: z.string() }).catchall(z.number());

UserSchema.parse({ name: "", age: 0 });     // ✅ 検証に成功します
UserSchema.parse({ name: "", age: false }); // ❌ 検証に失敗します

注意点としては、.catchall()オプションで検証されたプロパティは、無関係なプロパティとは扱われなくなりますので、.passthrough().strip().strict()で検知する事が出来無くなります。

.catchall()の影響についての例
const UserSchema = z.object({ name: z.string() }).catchall(z.number());

const values = { name: "", age: 0 };

// .strict()の場合
const result = UserSchema.strict().parse(values); // ✅ 検証に成功します
console.log(result); // { name: "", age: 0 }

// .passthrough()の場合
const result2 = UserSchema.passthrough().parse(values); // ✅ 検証に成功します
console.log(result2); // { name: "", age: 0 }

// .strip()の場合
const result3 = UserSchema.strip().parse(values); // ✅ 検証に成功します
console.log(result3); // { name: "", age: 0 }

Union型(共用型)

Union型のスキーマを作成するにはz.union()を使って定義します。実行する際は引数にZodTypeのみを含んだ配列を渡す必要があります。

Union型のスキーマを定義
import * as z from "zod";

z.union([z.string(), z.number()]);            // string | number
z.union([z.string().nullable(), z.number()]); // string | number | null

// ZodTypeに被りがある場合は統合されます
z.union([z.string(), z.string()]); // string

また、.or()オプションを用いる事で、他のZodTypeに付随させることが可能です。

.or()オプションを用いた例
const StrOrNum = z.string().or(z.number()); // string | number

Intersection型(交差型)

Intersection型のスキーマを作成するにはz.intersection()を使って定義します。実行する際は引数にZodTypeのみを含んだ配列を渡す必要があります。

Intersection型のスキーマを定義
import * as z from "zod";

const a = z.union([z.number(), z.string()]);  // number | string
const b = z.union([z.number(), z.boolean()]); // number | boolean

z.intersection(a, b); // number

また、.and()オプションを用いる事で、他のZodTypeに付随させることが可能です。

.and()オプションを用いた例
const MergeSchema = obj1.and(obj2); // { a: string } & { b: number }

注意として、多くの場合2つのオブジェクトをマージするなら.merge()オプションを使用する事をオススメします。Intersection型のスキーマには、Object型の便利なオプションなどが無いため、他のスキーマとの相性があまり良くありません。

Enum型

Enum型のスキーマには、Zod enumsNative enumsの二つ種類があります。

Zod enums

このZod enumsはZod独自のモノですが、Enum型を定義または検証する時に推奨されています。

このZod独自のenumを定義するには、z.enum()を使って定義します。
注意として、引数の値には文字列のみを含んだ配列かつ、直接値を渡す必要があります👇

Zod enumsの挙動の確認
import * as z from "zod"

z.enum(["Salmon", "Tuna", "Trout"]); // 'Salmon' | 'Tuna' | 'Trout'

// 以下のようには書けません🙅‍♂️🙅‍♀️
const fish = ["Salmon", "Tuna", "Trout"];
z.enum(fish); // ❌ 型エラーが発生します

また、.enumオプションを用いる事でenum値にアクセスすることができ、.optionsオプションを使う事で、Tuple型に変換できます。

Zod enumsのオプション活用例
const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);

type FishType = typeof FishEnum.enum;
/*
  {
    Salmon: "Salmon", 
    Tuna: "Tuna", 
    Trout: "Trout",
  }
*/

// EnumをTupleに変換する
type FishTuple = typeof FishEnum.options;
/*
  ["Salmon", "Tuna", "Trout"]
*/

Native enums

Native enumsのスキーマを定義するにはz.nativeEnum()を使って定義します。

サードパーティライブラリの列挙型に対して検証する必要がある場合は、こちらを使う事で対応可能です。

Numeric enums

enum Fruits {
  Apple,
  Banana,
}

const FruitEnum = z.nativeEnum(Fruits);
type FruitEnum = z.infer<typeof FruitEnum>; // Fruits

FruitEnum.parse(Fruits.Apple);  // ✅ 検証に成功します
FruitEnum.parse(Fruits.Banana); // ✅ 検証に成功します
FruitEnum.parse(0);             // ✅ 検証に成功します
FruitEnum.parse(1);             // ✅ 検証に成功します
FruitEnum.parse(3);             // ❌ 検証に失敗します

String enums

enum Fruits {
  Apple = "apple",
  Banana = "banana",
  Cantaloupe = 0, // Numeric enumsとString enumsは、混在させることができます
}

const FruitEnum = z.nativeEnum(Fruits);
type FruitEnum = z.infer<typeof FruitEnum>; // Fruits

FruitEnum.parse(Fruits.Apple);      // ✅ 検証に成功します
FruitEnum.parse(Fruits.Cantaloupe); // ✅ 検証に成功します
FruitEnum.parse("apple");           // ✅ 検証に成功します
FruitEnum.parse("banana");          // ✅ 検証に成功します
FruitEnum.parse(0);                 // ✅ 検証に成功します
FruitEnum.parse("Cantaloupe");      // ❌ 検証に失敗します

Const enums

as constを用いたオブジェクトも扱うことが出来ます。

const Fruits = {
  Apple: "apple",
  Banana: "banana",
  Cantaloupe: 3,
} as const;

const FruitEnum = z.nativeEnum(Fruits);
type FruitEnum = z.infer<typeof FruitEnum>; // "apple" | "banana" | 3

FruitEnum.parse("apple");      // ✅ 検証に成功します
FruitEnum.parse("banana");     // ✅ 検証に成功します
FruitEnum.parse(3);            // ✅ 検証に成功します
FruitEnum.parse("Cantaloupe"); // ❌ 検証に失敗します

Tuples型

Tuples型のスキーマを定義するにはz.tuple()を使って定義します。

引数にはZodTypeのみを含んだ配列しか渡せない事に注意してください!
また.itemsオプションを用いる事で、要素のスキーマにアクセスする事が出来ます👇

Tuples型のスキーマを定義
import * as z from "zod";

z.tuple([ z.string(), z.number() ]);            // [ string, number ]
z.tuple([ z.string(), z.number().nullable() ]); // [ string, number | null ]

// 要素のスキーマを取得
const ItemsSchema = z.tuple([ z.string(), z.number() ]).items;
type ItemsType = typeof ItemsSchema;
/*
  [ z.ZodString, z.ZodNumber ]
*/

Optional型

Optional型のスキーマを定義するにはz.optional()を使って定義します。

Optional型のスキーマを定義
import * as z from "zod";

z.optional(z.string()); // string | undefined

.optional()オプションを用いる事で、他のZodTypeに付随させることが可能です。
また.unwrap()オプションを用いる事で、元に戻すことが出来ます。

.options()オプションを用いた例
z.string().optional();          // string | undefined
z.string().optional().unwrap(); // string

// .unwrap()が返すスキーマは元のスキーマと同じ値になります
const StringSchema = z.string();
const OptionalString = StringSchema.optional();
OptionalString.unwrap() === StringSchema; // true

Nullables型

Nullables型のスキーマを定義するにはz.nullable()を使って定義します。

Nullables型のスキーマを定義
import * as z from "zod";

z.nullable(z.string()); // string | null

.nullable()オプションを用いる事で、他のZodTypeに付随させることが可能です。
また.unwrap()オプションを用いる事で、元に戻すことが出来ます👇

.nullable()オプションを用いた例
z.string().nullable();          // string | null
z.string().nullable().unwrap(); // string

// .unwrap()が返すスキーマは元のスキーマと同じ値になります
const StringSchema = z.string();
const NullableString = StringSchema.nullable();
NullableString.unwrap() === StringSchema; // true

Records型

Records型のスキーマを定義するにはz.record()を使って定義します。

import * as z from "zod";

z.record(z.number()); // { [k: string]: number }

数値キーに関しての注意

z.record()のキーの型はString型となっていますが、これをNumber型にしたい場合があるかもしれません。しかし、Zodはこれをサポートしていません。

理由は、JavaScriptはすべてのオブジェクトキーを内部の文字列に自動的にキャストするためサポートする意味がないためです。以下の挙動を見れば分かりやすいと思います👇

JavaScriptがオブジェクトKey
const testMap: { [k: number]: string } = {
  1: "one",
};

for (const key in testMap) {
  console.log(`${key}: ${typeof key}`); // prints: `1: string`
}

上記の挙動より、本来Number型のkeyfor..in文内ではString型に変換されています。README.mdにも言及がありますので、以下引用させてもらいます。

Since Zod is trying to bridge the gap between static and runtime types, it doesn't make sense to provide a way of creating a record schema with numerical keys, since there's no such thing as a numerical key in runtime JavaScript.
要約
Zodは静的な型とランタイムの型のギャップを無くしたいので、Number型のキーはサポートしません。

Maps型

Maps型のスキーマを定義するにはz.map()を使って定義します。

Maps型のスキーマを定義
import * as z from "zod";

z.map(z.string(), z.number()); // Map<string, number>

Mapの詳細については、以下のサイトを参照してください👇

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map

Sets型

Sets型のスキーマを定義するにはz.set()を使って定義します。

Sets型のスキーマを定義
import * as z from "zod";

z.set(z.string()); // Set<string>

Setの詳細については、以下のサイトを参照してください👇

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Set

Function型

Function型のスキーマを作成するにはz.function()を使って定義しますが、こちらのZodTypeは関数を検証するモノではなく、関数の入力と出力を検証するモノとなっています。

そのため、他のZodTypeのように.parse()を使用するのではなく、.implement()を用いて関数を定義する際に使用します。

import * as z from "zod";

// スキーマの定義方法
z.function();                      // () => unknown
z.function().args(z.number());     // (args_0: number) => unknown
z.function().returns(z.boolean()); // () => boolean

// 以下のようにも定義できます。
const returnSchema = z.string();
const argsSchema = z.tuple([ z.number(), z.number() ]);

z.function(argsSchema, returnSchema); 
// (args_0: string, args_1: number) => string

// スキーマが何も返さない事を示すにはz.void()を使用します
z.function().returns(z.void()); // () => void

// Functionスキーマを使って関数を定義する
const add = z.function()
  .args(z.number(), z.number())
  .returns(z.number())
  .implement((a, b) => {  
    return a + b; // a,bは検証済みの値
  });
  
add(1, 2);   // ✅ 実行できる
add("1", 2); // ❌ 検証に失敗してエラーが発生します

上記のadd()ようにFunctionスキーマを使って関数を実装する事で、コンパイル後のJavaScriptでも型の整合性を保つこと出来ます。これにより、検証コードとビジネスロジックを混同することなく、関数の入力と出力を簡単に検証できています。

また、引数や返り値のスキーマを取得する事もできます👇

const FuncSchema = z.function()
  .args(z.string(), z.number())
  .returns(z.boolean());

// スキーマの型の確認
type FuncType = z.infer<typeof FuncSchema>;
// (arg0: string, arg1: number) => boolean

// 引数のスキーマを取得
FuncSchema.parameters(); // ZodTuple<[ZodString, ZodNumber]>

// 返り値のスキーマを取得
FuncSchema.returnType(); // ZodBoolean

Promise型

Promise型のスキーマを定義するにはz.promise()を用いて定義します。

注意点として、Promise型のスキーマの.parse()は特別な挙動をします。
検証には、以下の2つが行われます。

  1. 入力値がPromiseインスタンスであるかチェックします。( .then().catch()を持つオブジェクト )
  2. Zodは.then()を用いて、検証ステップを既存のPromiseに追加します。

上記を踏まえて、スキーマの定義は以下のようにします👇

import * as z from "zod";

// Promiseスキーマを定義
const numberPromise = z.promise(z.number());

// ❌ 入力値がPromiseでないため、検証に失敗します
numberPromise.parse(0); // ZodError: Non-Promise type: number 

// Promiseを返します( 返されたPromise内でエラーが発生します )
numberPromise.parse(Promise.resolve("tuna")); // Promise<number>

// async/awiatを使って検証できます
const test = async () => {
  await numberPromise.parse(Promise.resolve("tuna"));
  // ZodError: Non-number type: string

  await numberPromise.parse(Promise.resolve(3.14));
  // => 3.14
  
  // 検証失敗時の結果を取得するには`.catch()`を使用します
  await numberPromise.parse("invalid").catch((errorResult) => {/* ... */})
};

Recursive Types(再帰型)

再帰的なスキーマを定義するにはz.lazy()を用いて定義します。

注意点として、TypeScriptの制限によりスキーマの型を静的に推測することはできません。そのため、型を手動で定義する必要があります👇

再帰的なスキーマを定義
import * as z from "zod";

interface Category {
  name: string;
  subcategories: Category[];
}

// cast to z.ZodSchema<Category>
const CategorySchema: z.ZodSchema<Category> = z.lazy(() =>
  z.object({
    name: z.string(),
    subcategories: z.array(CategorySchema),
  })
);

CategorySchema.parse({
  name: "People",
  subcategories: [
    {
      name: "Politicians",
      subcategories: [{ name: "Presidents", subcategories: [] }],
    },
  ],
}); // ✅ 検証に成功します

JSON Type

JSONの値を検証するには、以下のスニペットを使用することが出来ます。

type Literal = boolean | null | number | string;
type Json = Literal | { [key: string]: Json } | Json[];
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
const jsonSchema: z.ZodSchema<Json> = z.lazy(() =>
  z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
);

jsonSchema.parse(data);

循環データをZodに渡した場合、無限ループが発生する事に注意してください!

Instanceof

instanceof演算子を使ったスキーマも作る事が可能です。z.instanceof()でスキーマを定義します👇

instanceofのスキーマを定義
import * as z from "zod";

class Test {
  name: string;
}

// instanceofのスキーマを作成
const TestSchema = z.instanceof(Test);

const blob: any = "whatever";
TestSchema.parse(new Test()); // ✅ 検証に成功します
TestSchema.parse("blob");     // ❌ 検証に失敗します

Template Literal

.custom()を用いる事で、TypeScriptのTemplate Literalを扱うことができます。
以下のソースコードは、Issueの回答の引用です👇

Issueより引用
const literal = z.custom<`${number}.${number}.${number}`>((val) =>
  /^\d+\.\d+\.\d+$/g.test(val as string)
);

Custom Validation

独自のバリデーションを実装したい場合は、.refine()オプションを使う事で定義できます。

カスタムバリデーションを実装する
import * as z from "zod";

// 入力値がanyの場合
z.any().refine(
  (value: any) => value === "hoge",
  (value: any) => ({ message: `${value} is not "hoge"` })
);

// 入力値がstringの場合
z.string().refine(
  (value: string) => value === "hoge",
  (value: string) => ({ message: `${value} is not "hoge"` })
);

.refine()の第一引数に渡す関数内では、エラーを投げないようにして下さい!
エラーを投げた場合、.refine()はそのエラーを無視して検証が成功したものと扱います。

.superRefine()

.refine()では一つの検証情報しか返せませんでしたが、.superRefine()を使う事で複数の検証情報を返すことが出来ます👇

.superRefine()の実装例
const Strings = z.array(z.string()).superRefine((val, ctx) => {

  // 配列の要素数を検証して、検証結果を登録する
  if (val.length > 3) {
    ctx.addIssue({
      code: z.ZodIssueCode.too_big,
      maximum: 3,
      type: "array",
      inclusive: true,
      message: "要素が多すぎます😡",
    });
  }

  // 配列要素の重複を検証して、検証結果を登録する
  if (val.length !== new Set(val).size) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "重複は認められません",
    });
  }
});

共通のオプションについて

上記で解説したZodTypeには共通のオプションが用意されています。
この節では、それらのオプションについて解説したいと思います。

検証用のオプション(関数)などは、後述するため省かせて頂きます。

.default()

スキーマにデフォルト値を設定します。

デフォルト値を設定している場合は、undfinedを入力値として許容する事に注意してください!

.default()の使用例
const schema = z.string().default("default value");

type SchemaType = z.infer<typeof schema>; // string

const result = schema.parse(void 0);
console.log(result); // "default value"

const result2 = schema.parse("hoge");
console.log(result2); // "hoge"

.optional()

スキーマの入力値にundefinedを追加します。

.optional()の使用例
const schema = z.string().optional();

type SchemaType = z.infer<typeof schema>; // string | undefined

const result = schema.parse(void 0);
console.log(result); // undefined

const result2 = schema.parse("hoge");
console.log(result2); // "hoge"

.nullable()

スキーマの入力値にnullを追加します。

.optional()の使用例
const schema = z.string().nullable();

type SchemaType = z.infer<typeof schema>; // string | null

const result = schema.parse(null);
console.log(result); // null

const result2 = schema.parse("hoge");
console.log(result2); // "hoge"

.array()

スキーマをArray型に変換します。

.optional()の使用例
const schema = z.string().array();

type SchemaType = z.infer<typeof schema>; // string[]

const result = schema.parse([]);
console.log(result); // []

const result2 = schema.parse(["hoge"]);
console.log(result2); // ["hoge"]

Array型の詳細については前述してありますので、そちらを参照してください。

.or()

スキーマをUnion型に変換します。

.optional()の使用例
const schema = z.string().or( z.number() );

type SchemaType = z.infer<typeof schema>; // string | number

const result = schema.parse("hoge");
console.log(result); // "hoge"

const result2 = schema.parse(100);
console.log(result2); // 100

Union型の詳細については前述してありますので、そちらを参照してください。

.and()

スキーマをIntersection型に変換します。

.and()の使用例
const a = z.union([z.number(), z.string()]);  // number | string
const b = z.union([z.number(), z.boolean()]); // number | boolean

a.and(b); // number

Intersection型の詳細については前述してありますので、そちらを参照してください。

検証方法について

この節では、検証を実行するためのオプション(関数)について解説したいと思います🗃

.parse()

.parse()を実行する事で、入力値が期待する値かを検証することが出来ます。
検証成功時には、検証した値( 期待した型の値 )を返し、検証に失敗した場合はエラーが投げられます👇

.parse()の実行例
import * as z from "zod";

// String型のスキーマ
const StringSchema = z.string();

StringSchema.parse("fish"); // ✅ 検証に成功し、"fish"を返します
StringSchema.parse(12);     // ❌ 検証に失敗し、エラーが投げられます

// エラー情報を受け取るにはtry..catch文を使う必要があります
try {
  StringSchema.parse(true);
} catch(error) {
  console.error(error); // ZodError('Non-string type: boolean');
}

.parseAsync()

.refine().transform()を使った非同期スキーマを検証する場合、.parseAsync()を使って検証する必要があります。

.parseAsync()の実行例
import * as z from "zod";

// 非同期スキーマを作成
const HelloSchema = z.string().refine(async (val) => val === "hello");

(async () => {
  await HelloSchema.parseAsync("hello"); // ✅ 検証に成功し、"hello"を返します
  await HelloSchema.parseAsync("world"); // ❌ 検証に失敗し、エラーが投げられます
})();

.parseAsync()の返り値は、Promiseであることに注意してください!

.safeParse()

エラーを投げずに検証するには、.safeParse()を使う事で検証できます。

.safeParse()の実行例
import * as z from "zod";

// String型のスキーマ
const StringSchema = z.string();

const result = StringSchema.parse("fish"); // ✅ 検証に成功
console.log(result); // { success: true, data: "fish" }

const result = StringSchema.parse(12); // ❌ 検証に失敗
console.log(result); // { success: false, data: ZodError }

検証結果は、Union型であるため以下のような記述で型情報を適切に適用できます。

const result = stringSchema.safeParse("billie");
type ResultType = typeof result;
// { result: true; data: string } | { result: false; error: ZodError }

if (result.success === true) result.error; // 🛑 型エラーが発生します
if (result.success === false) result.data; // 🛑 型エラーが発生します

.safeParseAsync()

.refine().transform()を使った非同期スキーマでエラーを投げずに検証する場合、.safeParseAsync()を使って検証する必要があります。また、エイリアスとして.spa()があります。

.safeParseAsync()の実行例
import * as z from "zod";

// 非同期スキーマを作成
const HelloSchema = z.string().refine(async (val) => val === "hello");

(async () => {
  const result = await HelloSchema.safeParseAsync("hello"); // ✅ 検証に成功します
  console.log(result); // { success: true, data: "hello" }
  
  const result = await HelloSchema.parseAsync("world"); // ❌ 検証に失敗します
  console.log(result); // { success: false, error: ZodError }
})();


// 以下のように`.spa()`を用いても同じです
(async () => {
  const result = await HelloSchema.spa("hello"); // ✅ 検証に成功します
  console.log(result); // { success: true, data: "hello" }
  
  const result = await HelloSchema.spa("world"); // ❌ 検証に失敗します
  console.log(result); // { success: false, error: ZodError }
})();

Transformerについて

Zodには、検証した値を別の値に変換するTransformerという機能があります。
以下の例では、文字列を数値に変換しています👇

文字列を数値に変換して例
import * as z from "zod";

// 文字列から数値へ変換するスキーマ
const stringToNumber = z.string().transform((val) => myString.length);

const result = stringToNumber.parse("string"); // ✅ 検証が成功します
console.log(result); // 6

.transform()内では、エラーを投げないようにして下さい。

メソッドチェーンの順番について

上記のstringToNumberは、ZodEffectsのサブクラスのインスタンスであることに注意してください!z.string()と同じインスタンスではありません。

そのため、z.string()が持つ.email()などのオプションを適用するには.transform()を実行する前に実行する必要があります。

文字列オプションと.transform()を組み合わせる
const emailToDomain = z
  .string()
  .email()
  .transform((val) => val.split("@")[1]);

const result = emailToDomain.parse("colinhacks@example.com"); 
console.log(result); // "example.com"

// ❌ 以下のような書き方は出来ません
const emailToDomain = z
  .string()
  .transform((val) => val.split("@")[1]) // 返す値に.email()が無い!
  .email();

非同期のTransformer

Transformerを使って、非同期の変換処理を定義することが出来ます👇

非同期スキーマを定義
const IdToUser = z.string().uuid().transform(
  async (userId) => {
    return await getUserById(userId); // userIdからユーザー情報へ変換する
  }
);

上記のスキーマを検証するには.parseAsync()などを使う必要があります!

.refine()との連携

.refine()を使う事で変換後の値を検証することが出来ますが、これは上記の非同期的に変換したデータを検証する時に便利なモノとなっています👇

.refine()との連携する例
// ユーザーのスキーマを定義
const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
});

// ユーザーIDをユーザー情報に変換するスキーマ
const IdToUser = UserSchema.shape.id.transform(
  async (userId) => {
    return await getUserById(userId); // userIdからユーザー情報へ変換する
  }
);

IdToUser
 .refine(user => UserSchema.safeParse(user).success, "不正なユーザー情報です")
 .parse("example-user-id")
 .then(user => {
    console.log(user); // { id: "example-user-id", name: "example user" }
 })

.refine()の第一引数に渡す関数内では、エラーを投げないでください!

例えば、以下の処理は正しく検証できません👇

ダメな例
IdToUser
 .refine(user => {

    // .parse()は検証失敗時にエラーを投げるが、
    // .refine()はそれを検知しないため必ず検証に成功してしまう
    return UserSchema.parse(user)

  }, "不正なユーザー情報です")

Error Handlingについて

この節では、検証失敗時に返されるErrorクラスや、その扱い方について解説していきたいと思います🌌

※ 基本的に以下の公式ドキュメントを参照しています👇

https://github.com/colinhacks/zod/blob/master/ERROR_HANDLING.md

ZodError

検証失敗時、ZodはErrorクラスを継承したZodErrorクラスのインスタンスを返します。

このZodErrorには、検証の詳細情報を含んだissuesオプションがあり、このオプションを使う事で詳細なエラーハンドリングを行う事が可能です👇

try {
  z.never().parse(1); // ❌ 必ず検証に失敗します
} catch(zodError: z.ZodError) {

  // 検証に失敗した理由をログに表示
  console.error( zodError.issues );
  /*
    [
      {
        "code": "invalid_type", // 検証タイプ( タイプ一覧は下記参照 )
        "expected": "never",    // 期待した値の型
        "received": "number",   // 入力値の型
        "path": [],             // エラー箇所へのパス
        "message": "Expected never, received string" // エラーの詳細文
      },
      // ...
    ]
  */
}

// Zodのエラーコード一覧
type ZodIssueCode =
  | "invalid_type"
  | "custom"
  | "invalid_union"
  | "invalid_enum_value"
  | "unrecognized_keys"
  | "invalid_arguments"
  | "invalid_return_type"
  | "invalid_date"
  | "invalid_string"
  | "too_small"
  | "too_big"
  | "invalid_intersection_types";

IssueCodeの詳細な説明は、以下の公式ドキュメントを参照してください。

https://github.com/colinhacks/zod/blob/v3/ERROR_HANDLING.md#zodissuecode

エラー文のカスタマイズについて

一部のオプションでは、引数にカスタマイズしたエラー文を渡すことが出来ます。
以下に設定できるオプションの一覧と設定方法を載せておきますので、参考にして下さい👇

import * as z from "zod";

// .refine()の設定方法
z.any().refine((input) => input, "hoge");
z.any().refine((input) => input, { message: "hoge" });
z.any().refine((input) => input, (input) => ({ message: `${input} is not hoge` }));

// z.string()のオプション
z.string().min(5, "カスタマイズしたエラー文")
z.string().max(5, "カスタマイズしたエラー文");
z.string().length(5, "カスタマイズしたエラー文");
z.string().url({ message: "カスタマイズしたエラー文" });
z.string().uuid({ message: "カスタマイズしたエラー文" });
z.string().email({ message: "カスタマイズしたエラー文" });
z.string().nonempty({ message: "カスタマイズしたエラー文" });
z.string().regex(/.*/,{ message: "カスタマイズしたエラー文" });

// z.number()のオプション
z.number().min(5, "カスタマイズしたエラー文"); 
z.number().max(5, "カスタマイズしたエラー文"); 
z.number().int({ message: "カスタマイズしたエラー文" }); 
z.number().positive({ message: "カスタマイズしたエラー文" });
z.number().negative({ message: "カスタマイズしたエラー文" });
z.number().nonpositive({ message: "カスタマイズしたエラー文" }); 
z.number().nonnegative({ message: "カスタマイズしたエラー文" }); 

// z.array()のオプション
z.array(z.any()).min(5, "カスタマイズしたエラー文");
z.array(z.any()).max(5, "カスタマイズしたエラー文");
z.array(z.any()).length(5, { message: "カスタマイズしたエラー文" });

型エラーなどのエラー文のカスタマイズ

型エラーやオプションがそもそもないスキーマのエラー文を設定するには、z.setErrorMap()を使ってエラー文を設定することが出来ます👇

import * as z from "zod";

type ReturnValue = { message: string };
type Issue = Omit<z.ZodIssue, "message">;
type Context = { defaultError: string; data: any };

// エラー情報をカスタマイズする関数
const customErrorMap = (issue: Issue, ctx: Context): ReturnValue => {
  // 型情報で判定
  if (issue.code === z.ZodIssueCode.invalid_type) {
    if (issue.expected === "string") {
      return { message: "文字列型ではありません" };
    }
  }

  if (issue.code === z.ZodIssueCode.custom) {
    // 検証条件でエラー文を作成
    return { message: `${(issue.params || {}).minimum}以下です` };
  }
  
  // デフォルトの値を返す
  return { message: ctx.defaultError };
};

// customErrorMapを設定する
z.setErrorMap(customErrorMap);

注意点として、上記の設定は全ての検証に反映されます!
そのため、間違って他のスキーマのエラー文を変更してしまう可能性がありますので、z.setErrorMap()に渡す関数にはバグが無いようにしましょう。

型生成について

ZodTypeで生成したスキーマからTypeScriptの型を生成することが出来ます!
使い方は簡単で、z.infer<>の型引数にスキーマの型を入れるだけで生成できます!

z.infer<>の挙動確認
import * as z from "zod";

const A = z.string();
type A = z.infer<typeof A>; // string

const u: A = 12; // TypeError
const u: A = "asdf"; // compiles

Transformerとの兼ね合い

Transformerを使っている場合、入力値と出力値の型情報が違う事があります。
そのため、z.infer<>は以下のような挙動になる事に注意してください👇

Transformerの型生成の挙動
// string => number への変換スキーマ
const stringToNumber = z.string().transform(val => val.length)

// ⚠️ z.infer は出力値の型を生成します!
type type = z.infer<stringToNumber>; // number

// ℹ️ 出力値の型を生成するにはz.outputを使用します
type out = z.output<stringToNumber>; // number, z.inferと同等

// ℹ️ 入力値の型を生成するにはz.inputを使用します
type in = z.input<stringToNumber>; // string, 入力値の型を生成します

型引数の渡し方

Zodを段階的に導入したい人で、z.object()などに既に作成済みの型情報をz.object<AnyType>()のような型引数に渡す感じでスキーマを定義したい人が居るかもしれません。

これを行うには、z.object()に渡せるようにするtoZodを作成して以下のようにします👇

z.object()に型情報を渡す
import * as z from "zod";

// zodに渡せる型に変換する型
type toZod<T extends Record<string, any>> = {
  [K in keyof T]-?: z.ZodType<T[K]>;
}

interface Hoge {
  hello: string;
  world: string;
}

// Hogeを型引数として渡す
const HogeSchema = z.object<toZod<IHoge>>({ 
  hello: z.string(),
  world: z.string()
})

これにより、Hogeの変更内容をz.object()に伝える事が出来ます。
例えば、以下のようにHogeの要素を変更した場合、z.object()内で型エラーが発生します。

Hogeの要素を変更する
import * as z from "zod";

// zodに渡せる型に変換する型
type toZod<T extends Record<string, any>> = {
  [K in keyof T]-?: z.ZodType<T[K]>;
}

interface Hoge {
  hello: string;
  // worldをコメントアウト
  // world: string; 
}

// Hogeを型引数として渡す
const HogeSchema = z.object<toZod<IHoge>>({ 
  hello: z.string(),
  world: z.string() // ❌ 型エラーが発生します!
})

react-hook-formとの連携

フォームのバリデーションライブラリとして有名な react-hook-form との連携が可能です。
公式READMEにサンプルコードがありましたので、以下に引用します。

公式READMEより引用したサンプルコード
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';

const schema = z.object({
  name: z.string().nonempty({ message: 'Required' }),
  age: z.number().min(10),
});

const App = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: zodResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('name')} />
      {errors.name?.message && <p>{errors.name?.message}</p>}
      <input type="number" {...register('age', { valueAsNumber: true })} />
      {errors.age?.message && <p>{errors.age?.message}</p>}
      <input type="submit" />
    </form>
  );
};

export default App;

あとがき

以上、ここまでZodの基本的な使い方から詳細な挙動について紹介してきました。

この記事でZodを知る人が増える機会を作れたのなら本望です。

これが誰かの参考になれば幸いです。
もし記事に間違いなどがあれば、コメントなどで教えて頂けると嬉しいです。
ここまで読んでくれてありがとうございました🙏

さて、ここからは余談ですが、TypeScriptを怖いと言っていた知人に、この記事の解説部分のみを見せてみました。するとどうでしょう、「 あー、怖い、怖い😫 」と言いながら見るのを断るではありませんか!

まさかの反応に、「 えっ、本当に怖いの? 」と知人に問うと、

「 こんなの誰だって、読むの怖いでしょ。 」

と言ってきました。

「 えっ、怖くはないでしょ。技術記事なんだし。怖い要素一つもなくない? 」

と私が言うと、知人が気付いて私にこう言いました。

「 あれ?もしかして "こわい" を "怖い" と勘違いしてない? 」
「 "こわい" っていうのは、"だるい" っていう意味だったんだけど。。。 」

それを聞いて、「 あー、なるほど。」と今までの食い違いを理解しました。

その後話を聞くと、「 こわい 」は北海道の方言で「 だるい・疲れる 」と言う意味になるそうで、知人は「 エンジニアとして一番怖いモノは何? 」と言う私の質問を、「 エンジニアとして一番疲れるモノは何? 」と解釈して答えたようです。

そりゃ確かに、JavaScriptに比べたらTypeScriptは結構疲れる所はありますよね😅

解釈の食い違いも解決したところで一件落着!・・・ではありません😡

私はこの記事を書くために、休日のほとんどを潰しました。それなのに、こんなオチで許すほど私はお人好しではありません!

腹の虫がおさまらない私は、一矢報いるべく知人に再度問い質しました。

「 それじゃあ、本当に怖いモノって何? 」

すると、知人が一言。

俺を怖がらせようとするお前の行動力にゾッとしとるわ。

(´・ω・`) ...

それではまた👋

GitHubで編集を提案

Discussion

ログインするとコメントできます