はじめに
Web制作でSCSSのトランスパイルやNodeパッケージのビルド環境に触れていた経験から、TypeScriptもビルド時に通常のJavaScriptへ変換され、実行時には interface や type といった型情報が残らないこと自体はイメージできていました。
その上で、実際にお問い合わせフォームを設計・実装するにあたり、次の2つの疑問を整理したいと考えたのがバリデーションライブラリ Zod(ゾッド) を詳しく学ぶきっかけとなりました。
- HTMLの各フォーム部品に
requiredやmaxlength属性を指定するのと、Zodを使うのとでは何が違うのか。 - TypeScriptの型定義と、実行時の入力チェック処理を別々に書くと「二重管理」になって修正漏れが起きやすくなるのではないか。
本記事では、HTML属性とZodの役割の違いから、検証ルールとTypeScriptの型定義を1箇所(SSOT)にまとめる基本までを整理します。
1. HTMLのフォーム属性(required / maxlength)とZodの違い
フォームを作る際、HTML標準の <input required maxlength="100" type="email"> といった属性だけでも入力制限は可能ですが、実務やセキュリティの観点ではZodのようなプログラムによる検証と明確な違いがあります。
| 比較の観点 | HTML属性(required / maxlength 等) |
Zod によるスキーマ検証 |
|---|---|---|
| セキュリティ面(突破のしやすさ) | あくまでブラウザ画面上のガイドであり、検証ツール(DevTools)でのHTML書き換えやAPIへの直接送信によって簡単に素通りしてしまいます。 | ブラウザ側だけでなく サーバー側(APIの入り口)でも同じコードを実行できる ため、HTMLを改ざんした不正送信も確実に遮断できます。 |
| データの整形(前処理)と細かな検査 | スペースのみの入力判定や前後の空白除去が難しく、改行コード・ヌルバイト等の制御文字遮断や「文字列以外の型(配列等)が送られてきた場合」の検査ができません。 | .trim() で前後の空白を自動除去した上で文字数を測ったり、z.string() と正規表現で型汚染や制御文字の混入をきめ細かく防いだりできます。 |
| TypeScriptの型定義との連動 | HTMLの属性を変えてもTypeScriptの型には反映されないため、別途手作業で型や定数を合わせる必要があります。 | スキーマを1つ定義すれば、そこからTypeScriptの型や上限値定数を共有でき、修正漏れを防げます。 |
そのため、HTML属性が不要になるわけではなく、以下のように 3段階で役割を分担させる のが効果的です。
- HTML属性(
required/maxlength): 一般の訪問者が入力ミスにすぐ気づけるための 「最初の操作ガイド(UX)」 として指定します。 - フロントエンドの Zod: 送信前に空白除去(
trim)や文字種チェックを行い、わかりやすい日本語エラーメッセージを表示します。 - バックエンドの Zod: ブラウザを迂回して直接送られてきた不正データをAPIの入り口で弾く 「セキュリティの最後の砦」 として機能させます。
2. Zodの基本:検証ルールから「型」を自動生成する
TypeScriptの interface はコンパイル時に消えるため、本来であれば「開発用の型定義」と「実行用の if 文チェック」を別々に書く必要があります。
Zodを使うと、「どういうデータを受け入れるか」というスキーマを1つ定義するだけで、実行時の検証(safeParse)とTypeScriptの型抽出(z.input / z.output)の両方をまかなうことができます。
import { z } from "zod";
// 1. 入力チェックのルール(スキーマ)を定義する
export const ContactSchema = z.object({
name: z
.string()
.trim()
.min(1, "お名前を入力してください。")
.max(100, "100文字以内で入力してください。")
.regex(/^[^\x00-\x1F\x7F]*$/, "使用できない制御文字が含まれています。"),
email: z.string().trim().max(254).email("有効なメール形式で入力してください。"),
message: z.string().trim().min(10).max(3000),
});
// 2. スキーマからTypeScriptの型を自動で取り出す(手書きの interface が不要になる)
export type ContactPayload = z.input<typeof ContactSchema>;
将来「最大文字数を変更する」といった仕様変更があっても、ContactSchema を1箇所書き換えるだけで検証ロジックとTypeScriptの型定義が同時に更新されます。
3. フロントエンドとバックエンドで定義を共有する(SSOT)
さらに、この schema.ts を画面側(ブラウザ)と裏側(APIサーバー)の双方から読み込む 単一情報源(SSOT: Single Source of Truth) の構成にすると、システム全体でルールの食い違いが起きなくなります。
flowchart LR
subgraph SSOT["共通ファイル (schema.ts)"]
Schema["ContactSchema (検証ルール・文字数上限)"]
Types["ContactPayload (自動推論されたTS型)"]
Schema --> Types
end
HTML["HTMLフォーム属性 (maxlength等)"]
Client["フロントエンド (送信前チェック)"]
Server["バックエンド API (最終防衛ライン)"]
Schema -->|"上限値の参照"| HTML
Schema -->|"送信前の即時検証"| Client
Schema -->|"不正データ・制御文字の遮断"| Server
Types -->|"型安全なデータ受け渡し"| Client
Types -->|"型安全なリクエスト処理"| Server
まとめ
HTML標準のフォーム属性で画面上の入力補助を行いつつ、Zodを用いてフロントエンド・バックエンド・TypeScriptの型定義を1箇所(SSOT)に集約することで、使いやすさと堅牢性を両立できることを学びました。
この仕組みを実際に組み込んだお問い合わせフォームの全体設計については、制作実績の 型安全性と軽量化を意識した就職活動用ポートフォリオサイト でも紹介しています。