【入門編】Type Aliasで実現する「型レベルのバリデーション」:Zodとの比較と使い分け – TypeScript コア・型システムの基礎解析バイブル

こんにちは!TypeScriptの世界へようこそ。
日々フロントエンドからバックエンドまでコードを書いていると、「このデータ、本当に正しい形をしているかな?」と不安になること、ありますよね。

他の言語からやってきた開発者の中には、「Zodなどのランタイムバリデーションライブラリがあれば、TypeScriptの型なんていらないのでは?」と思う方もいるかもしれません。逆に、「型エイリアス(Type Alias)を使えば、コンパイル時だけで完璧なバリデーションができるはずだ!」と意気込む方もいるでしょう。

今回は、この「ランタイム(実行時)」と「静的型(コンパイル時)」の境界線に焦点を当て、Type Aliasだけでどこまでバリデーションを表現できるのか、その限界とZodとの鮮やかな使い分けについて、一緒に深く探求していきましょう。

ここをクリアすれば、TypeScriptの型システムの本質がグッと見えてきますよ。さあ、一緒にマスターしていきましょう!

—

1. そもそも「型」は実行時に消えるという現実

まず最初に、TypeScriptにおける最も重要な大前提を共有させてください。

> 「TypeScriptの型は、コンパイルされた瞬間にすべて消え去る」

私たちが書く `type` や `interface` は、JavaScriptの実行時には跡形もありません。これらはすべて「開発中やビルド時に、エディタやコンパイラがエラーを教えてくれるための設計図」です。

例えば、外部のAPIから以下のようなデータが JSON として送られてきたとします。

{
“id”: “123”,
“email”: “test@example.com”
}

これをTypeScriptの世界に迎え入れるとき、私たちはついついこう書きたくなりますよね。

type User = {
id: string;
email: string;
};

// 外部から取ってきたデータ(仮に any や unknown とする)
const rawData: unknown = fetchUserData();

// ★ ここで「User型だから大丈夫!」と信じ込んではいけない!
const user = rawData as User;

`as User`(型アサーション)は、TypeScriptのコンパイラに対して「私を信じて!このデータは絶対にUser型だから!」と目をつむらせる呪文にすぎません。
もし実際のJSONの `email` が `null` だったり、そもそもデータが空っぽだったりしても、コンパイラは何も文句を言いません。そして、実行時に突然 `Cannot read properties of undefined` とアプリがクラッシュすることになります。これがランタイムと静的型のギャップです。

—

2. Type Aliasでどこまで「型レベルのバリデーション」ができるか?

「じゃあ、TypeScriptのType Aliasだけで、もっと厳密なルールを決められないの?」
実は、近年のTypeScript(特に v4.1以降)の型システムは驚異的な進化を遂げており、型パズルと呼ばれる高度なテクニックを使えば、型レベルでかなり細かいバリデーションを行うことができます。

例えば、「必ず `@` を含むメールアドレスのような文字列しか受け付けない」というルールをType Aliasで表現してみましょう。

// テンプレートリテラル型を使った「メールアドレスっぽい文字列」の表現
type Email = T extends `${string}@${string}` ? T : never;

// 正しい例:コンパイル通る
const validEmail: Email<"taro@example.com"> = “taro@example.com”;

// エラーになる例:コンパイルエラー!
// Type ‘string’ is not assignable to type ‘never’.
const invalidEmail: Email<"invalid-email-format"> = “invalid-email-format”;

おぉ、すごい!型エイリアスと条件付き型(Conditional Types)を組み合わせることで、間違った文字列を代入した瞬間にコンパイルエラーにすることができました。

しかし、ここに「限界」があります

このアプローチには、実務において大きな弱点が2つあります。

1. 「静的なリテラル型」にしか効かない
上のコードで検証できたのは、コード上に直接書かれた文字列(`”taro@example.com”` など)だけです。ユーザーが画面の入力フォームに打ち込んだり、APIから動的に取得したりする `string` 型の変数に対しては、この型チェックはすり抜けてしまいます。
2. エラーメッセージが難解になる
複雑な型バリデーションを組めば組むほど、コンパイルエラーが起きたときのメッセージが暗号のように難しくなり、初学者だけでなくチームメンバー全員の認知負荷が跳ね上がります。

つまり、Type Aliasによるバリデーションは「コードを書く時点のミスを防ぐ(開発者体験の向上)」には最強ですが、「実行時に外部から流れ込んでくる不確実なデータを防ぐ」ことはできないのです。

—

3. Zodとの比較:ランタイムと静的型の融合

ここで登場するのが、Zodに代表されるランタイムバリデーションライブラリです。
Zodが素晴らしいのは、「ランタイムのバリデーション定義から、自動的にTypeScriptの型を逆算(推論)してくれる」という点にあります。

先ほどのUserの例を、Zodで書いてみましょう。

import { z } from ‘zod’;

// 1. ランタイムのバリデーションスキーマを定義する
const UserSchema = z.object({
id: z.string().uuid(), // UUID形式でなければならない
email: z.string().email(), // 正しいメールアドレスでなければならない
});

// 2. Zodのスキーマから、TypeScriptのType Aliasを「自動生成」する!
type User = z.infer;

// 生成されたUser型は、実質的に以下の型と同等になります:
// type User = {
// id: string;
// email: string;
// };

このアプローチの何が革命的か分かりますか?
「型を二重に定義する手間」が消えるだけでなく、「Zodが実行時にデータを検証し、通ったデータだけが、自動的に正しいUser型として扱われる」という安全なパイプラインが完成するのです。

const processUserData = (rawData: unknown) => {
// 実行時バリデーション(ここで不正なデータなら例外を投げる)
const result = UserSchema.safeParse(rawData);

if (!result.success) {
console.error(“データの形が不正です!”, result.error.format());
return;
}

// ここに到達した時点で、result.data は完全に安全な User 型として扱える!
const user: User = result.data;
console.log(`こんにちは、${user.email}さん!`);
};

—

4. 現場で迷わない!使い分けの黄金ルール

ここまで読み進めてくれたあなたなら、もう両者の役割の違いがハッキリと見えてきているはずです。最後に、実務で迷わないための使い分けの指針を整理しておきましょう。

| 比較項目 | Type Alias(静的型) | Zodなどのランタイムバリデーション |
| :— | :— | :— |
| 実行タイミング | コンパイル時(開発中・ビルド時) | 実行時(ブラウザやNode.jsでの稼働中) |
| 主な目的 | 開発者のミス防止、コードの意図の共有 | 外部からの不正なデータ(API, DB, フォーム)の排除 |
| データの信頼性 | 保証できない(あくまで「約束事」) | 絶対に保証される(バリデーション通過後) |
| パフォーマンス | 実行時コストゼロ(コードに残らない) | 実行時コストがわずかにある(検証処理の実行) |

💡 先輩からのアドバイス

  • アプリの内部で完結するロジックやコンポーネントのProps

⇒ Type Aliasだけで十分に美しく、堅牢に書けます。

  • ユーザーの入力、外部APIのレスポンス、ファイル読み込みなど「外の世界」と境界を接する場所

⇒ 必ず Zod などのランタイムバリデーションを挟み、その推論結果をType Aliasとして活用しましょう。

TypeScriptの型システムとランタイムバリデーションは、決して対立するものではなく、お互いの弱点を補い合う最高の相棒です。この境界線を意識できるようになれば、あなたの書くコードの堅牢性は一段と跳ね上がりますよ。

ここをクリアできれば、TypeScriptの基本はもうバッチリマスターです!自信を持って次の実装に進んでくださいね。それでは、また次回の記事でお会いしましょう!

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