【入門編】Mapped Typesを駆使した「型レベルのバリデーション」:Zodなしでスキーマを定義する – TypeScript コア・型システムの基礎解析バイブル

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!

タイトルとURLをコピーしました