こんにちは!TypeScriptの型システムの世界へようこそ。
日々フロントエンドからバックエンドまでコードを書いていると、「あれ、型安全を信じていたのに、実行時になぜエラーになるんだろう?」という瞬間に出会ったことはありませんか?
他のプログラミング言語(JavaやC#など)からやってきた開発者ほど、この罠にハマりがちです。今回は、TypeScriptの根幹に関わる「Interfaceのプライベートプロパティ問題とランタイムの限界」について、優しく、そして深く紐解いていきたいと思います。
ここをクリアすれば、TypeScriptの型安全性の本質が手に取るようにわかるようになりますよ。一緒にバッチリマスターしていきましょう!
—
1. まずはおさらい:Interfaceと型エイリアス(Type Alias)の基本
TypeScriptを書く上で、オブジェクトの「形」を定義するために欠かせないのが `interface` と `type` ですよね。
例えば、ユーザー情報を表すデータ構造を考えてみましょう。
// interfaceを使った定義
interface User {
id: string;
name: string;
}
// 型エイリアスを使った定義
type UserType = {
id: string;
name: string;
};
初学者のうちは、「どっちを使っても同じように動くじゃん!」と思うはずです。実際、オブジェクトの形状を定義するだけなら、大半のケースで互換性があります。
しかし、ここに TypeScript最大の「秘密」 が隠されています。
—
2. 衝撃の事実:TypeScriptのコードは「コンパイルすると消える」
他の言語(JavaやC++など)では、クラスやインターフェースの定義はコンパイル後もメタデータとして実行時に残り続けます。
しかし、TypeScriptの `interface` も `type` も、JavaScriptにトランスパイル(変換)された瞬間、跡形もなく消滅します。
【TypeScriptの世界(開発時)】
interface User { id: string; name: string; } ← 型チェックに使われる
↓ tsc(コンパイラ)によるトランスパイル ↓
【JavaScriptの世界(実行時)】
// User インターフェースの形跡はゼロ!ただのオブジェクトだけが残る
これこそが、TypeScriptの型安全性が持つ「限界」の正体です。TypeScriptは「コンパイル時にのみ存在する幻想のガードマン」なのです。実行時には、そのガードマンはもういません。
—
3. 陥りやすい罠:「Interfaceのプライベートプロパティ」という幻想
他の言語の感覚で、次のようなコードを書いたことはありませんか?
「外部から触られたくない機密データだから、Interfaceで隠蔽しよう」と。
// ❌ 誤解されたコード例
interface SecretAccount {
// 他の言語の private のようなイメージを持っていませんか?
private balance: number;
}
これを書くと、TypeScriptのコンパイラは即座にこう怒ります。
> 文法エラー (TS1039): `Modifiers cannot be used here.`
> (修飾子(privateなど)はここでは使用できません)
そう、TypeScriptの `interface` や `type` のプロパティには、`private` や `protected` といったアクセス修飾子を付与することができません(※クラスやES2022のプライベートフィールド `#` とは異なります)。
なぜなら、前述の通り `interface` は実行時に消えてしまうからです。実行時に存在しないものに「隠蔽」の概念を持たせることはできないのですね。
—
4. 現場で起きる悲劇:APIからの「偽物データ」
コンパイル時には完璧に型チェックされていても、実行時に外部(APIやLocalStorageなど)から予期せぬデータが送り込まれてきたらどうなるでしょうか?
interface AdminUser {
id: string;
role: ‘admin’;
}
function processAdmin(user: AdminUser) {
console.log(`Processing admin: ${user.id}`);
}
// 悪意ある、あるいは壊れたデータがAPI経由でやってきたとする
const rawData: unknown = { id: “123”, role: “guest” };
// TypeScriptはこのキャスト(型アサーション)を信用してしまう!
processAdmin(rawData as AdminUser);
// 【実行時の悲劇】
// コンパイルエラーは1行も出ない。
// だが、実行時に role が “guest” のままビジネスロジックが走り、システムがバグる!
TypeScriptは「`as AdminUser` とプログラマが言ったんだから、きっと正しいんだろう」と信じ込んでしまいます。しかし、実行時のJavaScriptはただのオブジェクトをそのまま通してしまいます。これが、型安全性の限界です。
—
5. 回避策:ランタイム型チェック(Type Guard)との融合
この限界を突破するためには、「TypeScriptの型(コンパイル時)」と「JavaScriptのバリデーション(実行時)」を合体させる必要があります。
ここで大活躍するのが、ユーザー定義の型ガード(Type Guards)です。
interface AdminUser {
id: string;
role: ‘admin’;
}
// 実行時にオブジェクトが本当に AdminUser なのかを検証する関数
function isAdminUser(value: unknown): value is AdminUser {
return (
typeof value === ‘object’ &&
value !== null &&
‘id’ in value &&
typeof (value as any).id === ‘string’ &&
‘role’ in value &&
(value as any).role === ‘admin’
);
}
// — 実際の使用例 —
const response: unknown = JSON.parse(‘{“id”: “999”, “role”: “admin”}’);
if (isAdminUser(response)) {
// このブロックの中では、TypeScriptは response が「確実に AdminUser である」と認識する!
processAdmin(response);
console.log(“安全に処理されました!”);
} else {
console.error(“不正なデータフォーマットです!”);
}
このコードの何がスゴいのか?
`isAdminUser` 関数の戻り値にある `value is AdminUser` という特殊な構文(Type Predicate)に注目してください。
これにより、実行時の `if` 文のチェックを通過した瞬間、TypeScriptのコンパイラが「あ、このデータは安全なんだな」と理解し、スコープ内の型を自動的に絞り込んでくれるのです。
実務では、手書きでバリデーションを書く代わりに、Zod や Valibot といったランタイムバリデーションライブラリとTypeScriptの `z.infer
—
まとめ:TypeScriptを真に掌握するために
いかがでしたでしょうか? 今回のポイントをギュッと凝縮して振り返ってみましょう。
1. `interface` や `type` は実行時には存在しない(コンパイル時に消える)。
2. そのため、インターフェースレベルでデータの隠蔽(プライベート化)や実行時の安全性は保証できない。
3. 外部からのデータ(APIやユーザー入力)をしごく時は、ランタイムのチェック(型ガードやバリデーションライブラリ)を必ず併用する。
「TypeScriptはコンパイル時、JavaScriptは実行時」という二つの世界の見取り図を頭の中に持てるようになると、エラーに直面したときも「今、どちらの世界で何が起きているのか?」がクリアにわかるようになります。
ここをクリアできれば、あなたのTypeScriptスキルは初学者から一歩抜け出し、アーキテクトの視点にグッと近づきますよ。
明日からのコーディングで、ぜひ「実行時の安全」を意識した設計を取り入れてみてくださいね!