【入門編】TypeScriptにおける「空オブジェクト型({})」の危険性 – TypeScript コア・型システムの基礎解析バイブル

こんにちは!TypeScriptの型システムの世界へようこそ。
日々、フロントエンドからバックエンドまでコードを書いていると、TypeScriptの「型」が持つ奥深さに驚かされることってありますよね。

他の言語(例えばJavaやC#など)からやってきた開発者ほど、「オブジェクトっぽい形だから、とりあえず `{}`(空オブジェクト型)を指定しておけば安全だろう」と直感しがちです。

ですが、ここがTypeScriptの型システムにおける最初の大きな罠であり、同時に非常に面白いポイントでもあります。
今回は、この `{}` 型が隠し持つ「危険すぎる正体」と、それを回避して堅牢なコードを書くための実践知を、優しく紐解いていきましょう。ここをクリアすれば、TypeScriptの基本はバッチリマスターできますよ!

—

1. `{}`(空オブジェクト型)とは何者なのか?

まずは、TypeScriptにおける `{}` がコード上でどう振る舞うのか、実際の動きを見てみましょう。
他の言語の感覚だと、「プロパティを一切持たない、ガリガリの空っぽのオブジェクトしか代入できない型」に見えますよね?

でも、TypeScriptのコンパイラは、以下のように判断します。

// 一見、「プロパティが何もないオブジェクト」を要求しているように見える
const obj: {} = { name: “TypeScript” }; // 诶!? エラーにならない……?

console.log(obj);

なんと、`{ name: “TypeScript” }` というプロパティ付きのオブジェクトを代入しても、コンパイルエラーになりません。
さらに驚くべきことに、次のようなものも代入できてしまいます。

const a: {} = “Hello, World!”; // 文字列もOK?
const b: {} = 42; // 数値もOK?
const c: {} = true; // 真偽値もOK?
const d: {} = []; // 配列もOK?
const e: {} = () => {}; // 関数もOK?

「えっ、嘘でしょう……?」と思いましたよね。
実はTypeScriptにおいて、`{}` 型は「null と undefined 以外の、ほぼすべての値を受け入れるトップレベルに近い型」として評価されます。

—

2. なぜ `{}` に何でも入ってしまうのか?(型システムの裏側)

なぜこんなシュールな現象が起きるのでしょうか?
その理由は、TypeScriptの型システムが「構造的型付け(Structural Subtyping)」を採用しているからです。

構造的型付けの世界では、「その値が何であるか(名前やクラス)」ではなく、「その値がどんな形(プロパティ)を持っているか」が重視されます。

  • `{}` 型の定義: 「プロパティを1つも持たない構造のデータ」を要求する。
  • JavaScript/TypeScriptのプリミティブ(文字列や数値など): プロパティを要求されたとき、それらは `toString()` や `valueOf()` といったプロパティを(プロトタイプチェーン経由で)持っている。

つまり、TypeScriptのコンパイラはこう解釈しています。
> 「君が渡したその文字列(または数値)、`{}` が要求しているプロパティ(ゼロ個のプロパティ)を、ちゃんと満たしているよね? だから型安全だよ!」

これが、`{}` が「何でも受け入れる巨大な網」になってしまうカラクリです。
そして、この挙動を知らずに「オブジェクト型」として使ってしまうと、意図しないバグや型安全性の崩壊を招く危険性があるのです。

—

3. 陥りがちな罠:関数の引数での誤用

例えば、次のような関数を書いてしまったとしましょう。

// ❌ 危険なコード例
function processUser(user: {}) {
// ユーザーのデータを処理するついでに、うっかり存在しないプロパティにアクセスしてしまった!
console.log(user.age); // ⚠️ コンパイルエラーにならない!?(※正確にはJSではundefinedになるが、型としては検知されない)
}

// 呼び出し側
processUser({ name: “Taro” }); // 文字列でも数値でも渡せてしまう

`user` には `name` しかないのに、`user.age` のような存在しないプロパティへのアクセスをTypeScriptがスルーしてしまいます。これでは、「型による安全性の保証」が台無しですよね。

—

4. 正しい型定義へのアプローチ:どう書くべきか?

では、私たちが本当にやりたかった「空っぽのオブジェクトだけを受け入れたい」「オブジェクトの型を厳密に定義したい」ときは、どうすれば良いのでしょうか?
シチュエーションに応じた正しい処方箋を見ていきましょう。

パターンA: 本当にプロパティを持たない「空のオブジェクト」を表現したい場合

もし、キーや値を追加させない、完全に空のオブジェクトを強制したい場合は、`Record` などのユーティリティ型を用いるのがモダンで堅牢なアプローチです。

// ✅ 「何もプロパティを持たない」を厳密に表現する
const strictEmpty: Record = {};

// strictEmpty.name = “test”; // ❌ 即座にコンパイルエラーになる!

パターンB: 通常の「オブジェクト型」を定義したい場合

もし「何かしらのプロパティを持つオブジェクト」を意図しているなら、素直に `object` 型を使うか、プロパティの形を明示的にインターフェースや型エイリアスで定義します。

// ✅ object型(プリミティブ値を除外した「オブジェクト全般」を表す)
const validObj: object = { name: “TypeScript” };
// const invalid: object = “string”; // ❌ これはしっかりエラーになる!

// ✅ さらに実用的な:プロパティを明示する型定義
type User = {
name: string;
age?: number; // あってもなくても良いプロパティ
};

const user: User = { name: “Hanako” }; // 完璧です!

—

まとめ:今日の学びを武器に変えよう

ここまでのポイントをギュッと凝縮して振り返ってみましょう。

1. `{}` 型の正体:
`{}` は「何もない」ではなく、`null` と `undefined` 以外のほぼすべての値(プリミティブ含む)を受け入れる寛容すぎる型である。
2. 原因:
構造的型付けのルール上、どんな値も「ゼロ個のプロパティ」という要件を満たしてしまうため。
3. 対策:
オブジェクトを扱いたいときは `{}` ではなく、`object` 型を使うか、具体的なプロパティ構造(`type` や `interface`)を定義する。本当に空にしたいときは `Record` を活用する。

TypeScriptの型は、私たちのコードの意図をコンパイラに正確に伝えるための「共通言語」です。
`{}` が持つこの特殊な挙動を頭の片隅に置いておくだけで、現場で遭遇する「なんでこの型エラーにならないんだ…?」という謎の現象を華麗に回避できるようになりますよ。

ここをクリアしたあなたなら、もう基本の型推論で迷うことは怖くありません。
明日からのコーディングも、型と共に鮮やかに楽しんでいきましょう!

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