はじめに
普段は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の設定ファイルを書きます
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
のスタートスクリプトに以下を追加します
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"tsc": "tsc", //追加
"build": "webpack" //追加
},
次に今回TypeScriptを各ファイルを用意します
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>";
}
});
フロント画面を作成します
<!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
実際に画面を確認すると以下のようになります
未入力の状態でエンターを押すとエラーが表示されるような単純なものになっています
今回はバリデーションエラーをzodで作成していきます
zodでバリデーションチェックをする
まずはzodをインストールします
$ npm install zod
TypeScirptを以下に変更します
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
うまくバリデーションが動きました
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
を修正します
<!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
export type User = {
name: string;
password: string;
};
次にTypeScriptも変更します
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
何も入力せずにボタンを押すとエラーが表示されます
今回は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
を追加します
export type User = {
name: string;
password: string;
age: string;
};
しかし、このままではzodのUserScheme
では型の変更がわからず、age
に対してバリデーションを追加しないといけないことが実行するまで開発者に伝わりません
事前に型エラーになるようにUserSchemeをいじります
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;
}
}
});
このようにすることで
UserSchemeがage
がないのでエラーになるようになりました
おわりに
parseするだけで一気に型のエラーを確認できるので個人的にはとてもシンプルにかけるなと思いました
同じような方を2つかかないといけないのでそこをどのようにメンテナンスするかは考えたほうが良さそうです
今回利用したリポジトリは以下になります
参考