【実務・中級編】Type Aliasを用いた「型レベルのバリデーション」:Zodを使わずに型で制約をかける – TypeScript コア・型システムの基礎解析バイブル

Zodは不要だ:TypeScriptの型システムを「コンパイル時のバリデータ」として使い倒す極意

多くの開発者が、外部データ(APIレスポンスやユーザー入力)の整合性を保つためにZodのようなランタイムバリデーションライブラリに頼り切っている。もちろん、それらは便利だ。だが、「型定義さえあれば、コンパイル時にバグを撲滅できる」というTypeScriptの本質を見失ってはいないか?

今日は、外部依存を最小限に抑え、TypeScriptの型システムそのものを「静的バリデータ」として機能させる、最高峰の設計パターンを伝授する。

—

1. 型エイリアスは「単なる別名」ではない

多くのエンジニアが犯す間違いは、`type`を単なるエイリアスとして使うことだ。しかし、TypeScriptの型システムは「集合論」に基づいて構築されている。

例えば、「1から100までの整数」という制約を型で表現したいとする。単なる`number`では広すぎる。ここでBranded Types(銘柄付き型)という設計パターンを導入する。

/

  • 外部からは見えない「タグ」を付与することで、
  • 型の安全性を担保するBranded Typeのパターン

/
type Brand = K & { __brand: T };

// 1から100までの整数のみを許可する型
type Percent = Brand;

// コンストラクタ関数(ランタイムでの検証をここで行う)
const createPercent = (value: number): Percent => {
if (value < 1 || value > 100) {
throw new Error(“Invalid range: Must be 1-100”);
}
return value as Percent;
};

この手法の肝は、「型レベルでは`Percent`だが、ランタイムでは`number`として扱える」という点だ。API層で一度`createPercent`を通せば、以降の関数では`number`のチェックを二度とする必要はない。型システムが「この数値は既に検証済みである」と証明してくれるからだ。

—

2. テンプレートリテラル型で「ドメイン知識」を型に埋め込む

APIのIDや特定のフォーマットを持つ文字列を扱う際、単なる`string`で管理するのは罪に近い。TypeScriptのテンプレートリテラル型を使えば、正規表現の複雑さを型に持ち込める。

/

  • “USR-xxxx” の形式のみを受け入れるID型

/
type UserId = `USR-${string}`;

function fetchUser(id: UserId) {
console.log(`Fetching ${id}…`);
}

// fetchUser(“123”); // ❌ コンパイルエラー: 型 ‘”123″‘ は型 ‘`USR-${string}`’ に割り当てられません。
fetchUser(“USR-4429”); // ✅ 成功

これは単なる文字列チェックではない。コンパイラが「この文字列は期待する形式か?」を静的に解析する。ロジックをコードの深部に潜り込ませる前に、境界線(エッジ)で型によって弾く。これが堅牢なシステムを構築する鉄則だ。

—

3. 「検証済み」を型として伝播させる

実務で最も恐ろしいのは、「検証したはずのデータ」が、レイヤーを跨ぐ過程で「未検証のデータ」として扱われ、二重チェックが発生したり、逆にチェック漏れが起きることだ。

以下は、非同期API連携における「検証済みデータ」の伝播例だ。

interface RawData {
id: string;
score: number;
}

// フィルタリング後の型を定義
type ValidatedData = {
id: UserId;
score: Percent;
};

const validateRawData = (data: RawData): ValidatedData => {
return {
id: data.id as UserId, // 必要に応じてここで精査を行う
score: createPercent(data.score),
};
};

// サービス層では常にValidatedDataを受け取るようにする
const processData = (data: ValidatedData) => {
// ここにはもうバリデーションロジックは不要
return `Score of ${data.id} is ${data.score}%`;
};

このように「生のデータ」と「検証済みのデータ」を型レベルで分離することで、「どの関数がバリデーション済みデータを要求しているか」が型定義だけで一目瞭然になる。

—

4. パフォーマンス上の注意点:型計算の爆発

最後に、アーキテクトとして警告しておく。
複雑な条件型(Conditional Types)や再帰的な型定義を多用しすぎると、TypeScriptの型チェックにかかる時間が指数関数的に増大する。

  • 極端な型計算を避ける: `infer` を使った複雑な再帰は、型推論の限界(recursion limit)を容易に超える。
  • インターフェースを優先: `type`よりも`interface`の方が、コンパイラが型キャッシュを利用しやすく、パフォーマンス面で有利な場合が多い。

結論:型は「ドキュメント」ではなく「契約」である

Zodを使うことが悪いわけではない。しかし、ライブラリに頼る前に「この制約は型システムで表現できないか?」と自問してほしい。

今回紹介したBranded Typesやテンプレートリテラル型を活用すれば、実行時のコストを最小限に抑えつつ、コンパイル時にバグの芽を確実に摘み取ることができる。

「コードが動くこと」は最低条件だ。「型が正しいことを保証すること」こそが、プロフェッショナルの仕事である。 さあ、君のプロジェクトの型定義を、もっと美しく、もっと強固なものに進化させてくれ。

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