5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-01-26

はじめに

普段は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の設定ファイルを書きます

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のスタートスクリプトに以下を追加します

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

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

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>";
  }
});

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

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

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

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

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

$ npm install zod

TypeScirptを以下に変更します

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

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

zodについての解説

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

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

  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
types/user.d.ts
export type User = {
  name: string;
  password: string;
};

次にTypeScriptも変更します

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

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

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

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

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

    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を追加します

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

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

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

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

おわりに

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

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

参考

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?