こんにちは。TypeScriptの深淵へようこそ。
今日は、多くの開発者が一度は悩み、そして「型システムの境界線」で立ち止まるテーマについてお話ししましょう。そう、「TypeScriptの型チェック」と「ランタイム(実行時)バリデーション」の役割分担についてです。
「型を書けばバリデーションも終わるんじゃないの?」と思ったあなた。その感覚は非常に鋭いですが、TypeScriptという言語の真の姿を知ることで、さらに一段階上のエンジニアへと進化できますよ。
—
1. 「コンパイル時の守り」と「実行時の守り」を切り分ける
まず、TypeScriptの最も重要な前提を思い出してください。「TypeScriptの型は、コンパイル(トランスパイル)後にすべて消え去る」ということです。
// コンパイル後のJavaScriptでは、インターフェースも型エイリアスも影も形もありません
interface User {
id: number;
name: string;
}
// 実行時のJSはただの {} です。
// APIから返ってきたデータが本当に { id: number, name: string } なのか、
// 実行時のTypeScriptは一切保証してくれません。
ここが初心者が最初にぶつかる壁です。コンパイル時は「このコードは正しいか?」をチェックしますが、実行時は「外部からやってきたデータは正しいか?」という全く別の戦いが待っています。
2. 型レベルバリデーションの「限界」と「野望」
TypeScriptの型システム(Mapped TypesやTemplate Literal Typesなど)を駆使すれば、ある程度のバリデーションは実現可能です。例えば、「メールアドレス形式の文字列」を無理やり型で表現することもできます。
type Email = `${string}@${string}.${string}`;
const myEmail: Email = “test@example.com”; // OK
const invalidEmail: Email = “hello”; // エラー!型レベルでは防げる
一見便利そうですよね? しかし、これはあくまで「開発者のエディタ上」での防波堤に過ぎません。ユーザーがフォームに入力した文字列や、バックエンドから返ってきたJSONが `invalidEmail` だった場合、実行時のJSはそれを素通りさせてしまいます。
結論:型システムだけでバリデーションを完結させるのは、無理があるのです。
3. Zodと型定義を「同期」させる:究極の解法
そこで登場するのが Zod です。Zodは「スキーマ(データの形)」を定義し、それを元に「型」と「バリデーション関数」の両方を生成する、いわば魔法の杖のようなライブラリです。
「手書きの型定義」と「バリデーションロジック」を別々に管理すると、必ずいつか乖離(デグレ)が起きます。Zodを使えば、その悩みから解放されます。
実践:Zodによる型と実行時の安全性の両立
import { z } from “zod”;
// 1. スキーマを定義する(これが真実のソースコード)
const UserSchema = z.object({
id: z.number(),
name: z.string().min(2), // 実行時に「2文字以上」をチェック
});
// 2. スキーマからTypeScriptの型を「抽出」する(型定義の自動生成!)
type User = z.infer
// 3. 実行時のバリデーションを行う
const rawData = { id: 1, name: “A” }; // nameが短すぎる!
const result = UserSchema.safeParse(rawData);
if (!result.success) {
// 実行時エラーを丁寧にハンドリングできる
console.error(result.error.format());
} else {
// result.data は型安全な User 型として扱える
const user: User = result.data;
console.log(“Welcome, ” + user.name);
}
なぜこの手法が最強なのか?
- DRY原則の体現: 型定義とバリデーションロジックが一つに統合されています。スキーマを変えれば、型も自動的に追従します。
- ランタイムの安全性: `safeParse` を通すことで、未知の外部データが「本当に期待した型か」を検査できます。
- 型推論の恩恵: `z.infer` を使うことで、手書きによる型のミスをゼロにできます。
4. 陥りやすい罠:過剰な型定義の禁止
初心者の頃によくあるミスが、「何でもかんでも型でバリデーションしようとして、型定義が複雑怪奇になること」です。
例えば、複雑すぎる再帰的な型や、条件分岐だらけの型定義は、コンパイラの負荷を上げ、エディタの補完を重くし、自分自身の首を絞めることになります。
「難しいことは型にやらせず、実行時にライブラリにやらせる」。この引き算の美学こそが、熟練したTypeScriptエンジニアの流儀です。
—
最後に:型を「武器」として使いこなすために
TypeScriptの型システムは、あなたのコードを「より予測可能にするための防具」です。しかし、実行時の予測不能な入力(外部APIやユーザー入力)という荒波に対しては、Zodのようなランタイムバリデーションという「武器」を併用してください。
この2つを正しく組み合わせる感覚を掴めば、あなたはもうTypeScriptの入り口を完全に突破したと言っても過言ではありません。
「型はコンパイル時に消える」という事実に怯えるのではなく、それを逆手に取って、ランタイムで輝く安全なコードを書いていきましょう。ここをクリアすれば、開発の景色がガラリと変わるはずですよ。応援しています!