TypeScriptの深淵へようこそ:Zodなしで「型レベルのバリデーション」を実装する
TypeScriptを学び始めると、誰もが一度はこう思うはずです。「型って、ただのデータのラベルじゃないの?」と。
しかし、TSの本質は「コンパイル時にコードの正しさを数学的に証明する装置」にあります。今回は、外部ライブラリ(Zodなど)に頼らず、TypeScriptの型システムそのものを駆使して、堅牢なスキーマ定義を行う「Mapped Typesによる型レベルバリデーション」の世界を覗いてみましょう。
ここをマスターすれば、あなたのコードは「実行時にエラーで落ちる」回数が劇的に減り、開発体験が別次元のものになりますよ。
—
1. Mapped Types(マップ型)の正体
Mapped Typesは、ある型から「ルールに従って新しい型を作り出す」ための強力なテンプレートエンジンです。
type User = {
id: number;
name: string;
email: string;
};
// 全てのプロパティを読み取り専用(readonly)にする例
type ReadonlyUser = {
[K in keyof User]: User[K]; // Kはキー、User[K]はその値の型
};
この `[K in keyof T]` という構文は、「Tのキーを一つずつ取り出して処理する」という意味です。これを使うと、オブジェクトの構造を自由自在に再構築できるんです。
—
2. 実践:型レベルで「必須項目」と「更新用項目」を分ける
開発現場でよくあるのが、「新規作成時は全項目必須だが、更新時は一部だけでも良い」というスキーマ定義ですよね。これを毎回手書きするのはナンセンスです。
Mapped Typesを使えば、たった数行で自動生成できます。
type BaseUser = {
username: string;
email: string;
age: number;
};
// 1. 全てをオプショナル(任意)にする型
type PartialUser = {
[K in keyof BaseUser]?: BaseUser[K];
};
// 2. 更新用スキーマ(特定のキーを抽出することも可能)
type UpdateUserRequest = PartialUser;
const userUpdate: UpdateUserRequest = {
email: “new-email@example.com”, // これだけで型エラーにならない!
};
—
3. 型レベルバリデーション:特定の制約を強制する
さて、ここからが本題です。単なる変換ではなく、「特定の条件を満たしているか」を型でチェックする方法です。例えば、「特定のキーには特定の型しか許さない」といったルールを強制してみましょう。
// 値が必ず string であることだけを許可する型定義
type StringOnlySchema
[K in keyof T]: T[K] extends string ? T[K] : never;
};
// これを使うと…
type ValidConfig = StringOnlySchema<{
name: "John";
role: "admin";
}>;
// type InvalidConfig = StringOnlySchema<{ // name: "John"; // age: 25; // ❌ Error: Type 'number' does not satisfy the constraint 'string' // }>;
この `T[K] extends string ? T[K] : never` という記述がミソです。もし `string` 以外が混ざっていたら、`never`(決して存在しない型)に変換されるため、TypeScriptがコンパイル時に「そんなデータは不正だ!」と怒ってくれるわけです。
—
4. 陥りやすい罠:Mapped Typesの注意点
初心者が最初につまずくポイントが、「Mapped Typesの結果は、見た目以上に複雑になることがある」ということです。
- エラーメッセージが難解になる: `never` が混ざると、エラー文に `never` が頻出し、どこで間違えたか分かりにくくなります。
- 型の再帰: あまりに複雑なMapped Typesを重ねすぎると、コンパイラが「型評価の深さが限界です(Recursion depth limit)」と匙を投げることがあります。
解決のヒント: 複雑な型を作る時は、一度に全部やろうとせず、小さなUtility Typeに分解して名前をつけてあげてください。「型にも可読性が必要」ということを意識するだけで、あなたのコードはぐっと洗練されます。
—
まとめ:型は「守り」ではなく「攻め」の武器
多くの人は型を「エラーを防ぐための重い鎖」だと考えがちですが、実際は違います。型は、あなたがコードを書くときに何をすべきかを示してくれる最強のナビゲーターです。
今回紹介したMapped Typesによるスキーマ定義は、TypeScriptの深淵への入り口に過ぎません。
1. `keyof` で型からキーを取り出す
2. `Mapped Types` でルールを適用する
3. `Conditional Types`(`extends ? :`)で条件判定を行う
この3ステップを使いこなせば、Zodなどのランタイムバリデーションと組み合わせることで、フロントエンドからバックエンドまで、まさに「鉄壁」のデータ構造を構築できるようになります。
まずは、今のプロジェクトにある「手書きの型定義」を、このMapped Typesで自動化できないか試してみてください。その瞬間、TypeScriptがただの「チェックツール」から「設計パートナー」に変わるはずですよ。
それでは、また次回の深淵でお会いしましょう。Happy Hacking!