Zodは不要だ:TypeScriptの型システムを「コンパイル時のバリデータ」として使い倒す極意
多くの開発者が、外部データ(APIレスポンスやユーザー入力)の整合性を保つためにZodのようなランタイムバリデーションライブラリに頼り切っている。もちろん、それらは便利だ。だが、「型定義さえあれば、コンパイル時にバグを撲滅できる」というTypeScriptの本質を見失ってはいないか?
今日は、外部依存を最小限に抑え、TypeScriptの型システムそのものを「静的バリデータ」として機能させる、最高峰の設計パターンを伝授する。
—
1. 型エイリアスは「単なる別名」ではない
多くのエンジニアが犯す間違いは、`type`を単なるエイリアスとして使うことだ。しかし、TypeScriptの型システムは「集合論」に基づいて構築されている。
例えば、「1から100までの整数」という制約を型で表現したいとする。単なる`number`では広すぎる。ここでBranded Types(銘柄付き型)という設計パターンを導入する。
/
- 外部からは見えない「タグ」を付与することで、
- 型の安全性を担保するBranded Typeのパターン
/
type Brand
// 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やテンプレートリテラル型を活用すれば、実行時のコストを最小限に抑えつつ、コンパイル時にバグの芽を確実に摘み取ることができる。
「コードが動くこと」は最低条件だ。「型が正しいことを保証すること」こそが、プロフェッショナルの仕事である。 さあ、君のプロジェクトの型定義を、もっと美しく、もっと強固なものに進化させてくれ。