モダンなバリデーションライブラリ「Zod」ハンズオン【TypeScript】

# はじめに

普段はReactをUIフレームワークなどを利用してバリデーションの表示を行っています
フレームワークを利用していると簡単にバリデーションを利用できますが、利用しなくてもシンプルにバリデーション処理がかける`zod`について紹介します

ネットを見かけるとモダン開発というワードと一緒に`zod`という名前を見る機会が多くなりました。まったく何ができるものなのかも知らなかったので軽くハンズオンをしながら理解していきます

nullを許容しない、型をスキーマから作れる、バリデーションのカスタマイズがしやすいなどzodにはメリットがあるようです

# 開発環境の準備

```
$ mkdir zod-ts
$ cd zod-ts

# typescriptのインストール
$ sudo npm install -g typescript

# tsconfig作成
$ tsc --init

# package.json作成 (すべてエンター)
$ npm init

# webpackのインストール
$ npm install --save-dev webpack webpack-cli ts-loader

$ touch webpack.config.js
```

次にWebpackの設定ファイルを書きます

```js:webpack.config.js
const path = require("path");

module.exports = {
  // モード値を production に設定すると最適化された状態で、
  // development に設定するとソースマップ有効でJSファイルが出力される
  mode: "development", // "production" | "development" | "none"

  // メインとなるJavaScriptファイル（エントリーポイント）
  entry: "./sample.ts",

  output: {
    path: path.join(__dirname, "dist"),
    filename: "index.js",
  },

  module: {
    rules: [
      {
        // 拡張子 .ts の場合
        test: /\.ts$/,
        // TypeScript をコンパイルする
        use: "ts-loader",
      },
    ],
  },
  // import 文で .ts ファイルを解決するため
  resolve: {
    modules: [
      "node_modules", // node_modules 内も対象とする
    ],
    extensions: [
      ".ts",
      ".js", // node_modulesのライブラリ読み込みに必要
    ],
  },
};
```

`package.json`のスタートスクリプトに以下を追加します

```json:package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "tsc": "tsc", //追加
    "build": "webpack" //追加
  },
```

次に今回TypeScriptを各ファイルを用意します

```ts:sample.ts
let input = document.getElementById("input")! as HTMLInputElement;
input.addEventListener("change", (e) => {
  let value = input.value;
  if (value == "") {
    document.getElementById("valid")!.innerHTML =
      "<span style='color: red;'>未入力です</span>";
  }
});
```

フロント画面を作成します

```html:index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="text" id="name" placeholder="Enter your name" />
    <br />
    <div id="valid"></div>
    <script src="./dist/index.js"></script>
  </body>
</html>
```

次にTypeScriptを`Js`にコンパイルします

```
$ npm run build
```

実際に画面を確認すると以下のようになります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/02916df1-5bb6-c614-6a89-8ac2f492759a.png)

未入力の状態でエンターを押すとエラーが表示されるような単純なものになっています
今回はバリデーションエラーをzodで作成していきます

# zodでバリデーションチェックをする

まずはzodをインストールします

```
$ npm install zod
```

TypeScirptを以下に変更します

```ts:sample.ts
import { z } from "zod";

const name = z.string().min(1, { message: "名前が未入力です" });

let input = document.getElementById("input")! as HTMLInputElement;
input.addEventListener("change", (e) => {
  let value = input.value;
  try {
    name.parse(value);
  } catch (err) {
    if (err instanceof z.ZodError) {
      document.getElementById("valid")!.innerHTML =
        "<span style='color: red;'>" + err.issues[0].message + "</span>";
    }
  }
});
```

実行します

```
$ npm run build
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/801952a7-dd02-9cff-ac7b-344e56d93575.png)

うまくバリデーションが動きました

# zodについての解説

```ts
const name = z.string().min(1, { message: "名前が未入力です" });
```

名前に対してバリデーションのスキーマを定義しています。今回名前(name)はstringで1文字以上としています。1文字以下の場合`message`がエラーとして返ってきます

```ts
  try {
    name.parse(value);
  } catch (err) {
    if (err instanceof z.ZodError) {
      document.getElementById("valid")!.innerHTML =
        "<span style='color: red;'>" + err.issues[0].message + "</span>";
    }
```

`name.parse(value)`をすることでバリデーションのチェックができます
もしバリデーションエラーの場合`Throw`されるので`catch`してエラー処理をします
エラーメッセージは`err.issues`の0要素目(複数のエラーを配列にいれることができます)の`message`を取り出して、HTML要素として追加しています

# User型に対してバリデーションチェック

いまは`name`に対してバリデーションチェックをしましたが、`password`も追加してみます。個人的に複数のフォームがある際にきれいにかけるのが良いなと思いました

まずは`index.html`を修正します

```index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="text" id="name" placeholder="Enter your name" />
    <br />
    <input type="text" id="password" placeholder="Enter your password" />
    <br />
    <button id="button">Submit</button>
    <div id="valid"></div>
    <div id="user"></div>
    <script src="./dist/index.js"></script>
  </body>
</html>
```

次に`User型`を用意します

```
$ mkdir types
$ touch types/user.d.ts
```

```ts:types/user.d.ts
export type User = {
  name: string;
  password: string;
};
```

次にTypeScriptも変更します

```ts:sample.ts
import { z } from "zod";
import { User } from "./types/user";

const UserScheme = z.object({
    name: z
    .string()
    .min(1, { message: "名前が未入力です" })
    .max(20, { message: "名前は20文字以下で入力してください。" }),
    password: z.string().min(1, { message: "パスワードが未入力です" }),
  })
);

let name = document.getElementById("name")! as HTMLInputElement;
let password = document.getElementById("password")! as HTMLInputElement;
let button = document.getElementById("button")!;

button.addEventListener("click", (e) => {
  try {
    UserScheme.parse({ name: name.value, password: password.value });
    const user: User = { name: name.value, password: password.value };
    document.getElementById("user")!.innerHTML =
      "<span style='color: green;'>" + JSON.stringify(user) + "</span>";
  } catch (err) {
    if (err instanceof z.ZodError) {
      const message_list = err.issues.map((issue) => {
        return "<span style='color: red;'>" + issue.message + "</span>";
      });
      const message = message_list.join("<br>");
      document.getElementById("valid")!.innerHTML = message;
    }
  }
});
```

実行します

```
$ npm run build
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/432ef8b0-c0e6-46d0-a2bf-70ff4e7f5c2d.png)

何も入力せずにボタンを押すとエラーが表示されます

今回はUser型でzodのスキーマを定義しました

```ts
const UserScheme = z.object({
    name: z
    .string()
    .min(1, { message: "名前が未入力です" })
    .max(20, { message: "名前は20文字以下で入力してください。" }),
    password: z.string().min(1, { message: "パスワードが未入力です" }),
  })
);
```

パスワードも1文字以上入力されていないとエラーになります

```ts
    if (err instanceof z.ZodError) {
      const message_list = err.issues.map((issue) => {
        return "<span style='color: red;'>" + issue.message + "</span>";
      });
      const message = message_list.join("<br>");
      document.getElementById("valid")!.innerHTML = message;
    }
```

エラーは配列になっているのでmapで回してエラー内容だけを取り出してHTMLを構築しました

# User型に要素を追加する

ではここでUser型に`age`を追加します

```ts:types/user.ts
export type User = {
  name: string;
  password: string;
  age: string;
};
```

しかし、このままではzodの`UserScheme`では型の変更がわからず、`age`に対してバリデーションを追加しないといけないことが実行するまで開発者に伝わりません
事前に型エラーになるようにUserSchemeをいじります

```ts:sample.ts
import { z } from "zod";
import { User } from "./types/user";

// User型をZodTypeに変換して型として使う
const schemaForType =
  <T>() =>
  <S extends z.ZodType<T, any, any>>(arg: S) => {
    return arg;
  };

const UserScheme = schemaForType<User>()(
  z.object({
    name: z
      .string()
      .min(1, { message: "名前が未入力です" })
      .max(20, { message: "名前は20文字以下で入力してください。" }),
    password: z.string().min(1, { message: "パスワードが未入力です" }),
  })
);

let name = document.getElementById("name")! as HTMLInputElement;
let password = document.getElementById("password")! as HTMLInputElement;
let button = document.getElementById("button")!;

button.addEventListener("click", (e) => {
  try {
    UserScheme.parse({ name: name.value, password: password.value });
    const user: User = { name: name.value, password: password.value };
    document.getElementById("user")!.innerHTML =
      "<span style='color: green;'>" + JSON.stringify(user) + "</span>";
  } catch (err) {
    if (err instanceof z.ZodError) {
      const message_list = err.issues.map((issue) => {
        return "<span style='color: red;'>" + issue.message + "</span>";
      });
      const message = message_list.join("<br>");
      document.getElementById("valid")!.innerHTML = message;
    }
  }
});
```

このようにすることで

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/05f2a9d2-6515-f72e-6974-1c0bb3e54a0b.png)

UserSchemeが`age`がないのでエラーになるようになりました

# おわりに

parseするだけで一気に型のエラーを確認できるので個人的にはとてもシンプルにかけるなと思いました
同じような方を2つかかないといけないのでそこをどのようにメンテナンスするかは考えたほうが良さそうです

今回利用したリポジトリは以下になります

https://github.com/jinwatanabe/ts-zod-tutorial

# 参考

https://maasaablog.com/development/frontend/zod/5726/

https://reffect.co.jp/react/zod-validation

https://zenn.dev/ynakamura/articles/65d58863563fbc#%E8%A7%A3%E6%B1%BA%E7%AD%962%EF%BC%88recommended%EF%BC%89-schemefortype-utility-function%E3%82%92%E5%AE%9A%E7%BE%A9%E3%81%99%E3%82%8B