【入門編】Interfaceのプライベートプロパティ問題:TypeScriptの型安全性の限界と回避策 – TypeScript コア・型システムの基礎解析バイブル

こんにちは!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スキルは初学者から一歩抜け出し、アーキテクトの視点にグッと近づきますよ。
明日からのコーディングで、ぜひ「実行時の安全」を意識した設計を取り入れてみてくださいね!

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