こんにちは!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
// 正しい例:コンパイル通る
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の基本はもうバッチリマスターです!自信を持って次の実装に進んでくださいね。それでは、また次回の記事でお会いしましょう!