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

こんにちは!TypeScriptの世界へようこそ。
日々バリバリとコードを書いていると、型定義のちょっとした違和感につまづくことってありますよね。

他の言語(例えばJavaやC#、Pythonなど)からTypeScriptに入ってきた開発者が、もっとも最初に遭遇し、そして最も深く沼にハマりやすい罠が、今回取り上げる「空オブジェクト型:`{}`」です。

「何もプロパティを持たない空のオブジェクトだから、 `{}` って書けばいいんでしょ?」
そう思ってコードを書いていると、ある日突然、本来ならエラーになってほしいバグがすり抜けていく……そんな恐怖を味わうことになります。

ここをクリアすれば、TypeScriptの型システムがどういう思想で動いているのかがグッと見えてきますよ。さあ、一緒にその深淵を覗いてみましょう!

—

1. なぜ `{}` は危険なのか?(衝撃の事実)

まずは、次のコードを見てください。変数 `data` に `{}` という型をつけています。

const data: {} = “こんにちは、世界!”; // え、エラーにならない!?

……どうでしょうか? 「文字列(`string`)」を代入しているのに、TypeScriptのコンパイラはこれを何食わぬ顔でスルーします。
さらに、こんなものも代入できてしまいます。

const num: {} = 42; // 数値もOK
const bool: {} = true; // 真偽値もOK
const arr: {} = [1, 2, 3]; // 配列もOK
const func: {} = () => {}; // 関数もOK

「嘘でしょ!? オブジェクト型って言ったのに、なんでプリミティブ型(数値や文字列)まで入っちゃうの?」って思いますよね。

理由:TypeScriptにおける `{}` の本当の意味

TypeScriptの型システムにおいて、 `{}` は「プロパティを持たないオブジェクト」を表しているのではありません。

正確な定義は、「`null` と `undefined` 以外のすべてのプリミティブ値とオブジェクトを受け入れる、トップレベルに近い巨大な型」 です。

頭の中で、次のような階層(イメージ図)を思い浮かべてみてください。

[ 圧倒的な許容範囲:unknown ]
↓
[ null / undefined 以外のすべて:{} ] ← コレです!
↓
[ 厳密なオブジェクト:object / Record ]

JavaScriptの歴史的背景として、JavaScriptでは `typeof null` が `”object”` になったり、プリミティブ値に対しても `.toString()` などのメソッドが呼べたりします(ボクシングという仕組みですね)。
TypeScriptの初期の設計において、 `{}` は「`null` と `undefined` 以外の、プロパティアクセスやメソッド呼び出しが可能なすべての値」を包み込む型として定義されました。これが、現代の開発において罠になる理由です。

—

2. 実務でよくある「やっちまった」コード

実務の現場で、APIからのレスポンスや、関数の引数の型にうっかり `{}` を使ってしまったときの恐ろしい例を見てみましょう。

// 「引数には何かオブジェクトを渡してね」のつもりで {} を使った関数
function printUserId(user: {}) {
// 開発者の意図:user.id を表示したい
console.log(user.id); // ⚠️ ここでコンパイルエラーになる!
}

// でも、呼び出し側でこう書けてしまう
printUserId(12345); // 数値を渡しちゃった!

ここで2つの問題が発生しています。
1. `user.id` を取得しようとすると、TypeScriptから `Property ‘id’ does not exist on type ‘{}’.` というお叱りを受けます(`{}` には `id` なんて定義されていないため)。
2. なのに、呼び出し側では `printUserId(12345)` のように数字を渡してもエラーになりません。

これでは型安全の恩恵が台無しですよね。意図したオブジェクトを受け取りたい場合は、 `{}` を使うべきではありません。

—

3. 回避策:私たちが本当に使うべき「2つの騎士」

では、この危険な `{}` の代わりに、実務では何をどう使い分ければいいのでしょうか?
答えは明確で、主に次の2つを使い分けます。

1. `object` 型 (小文字の o)
2. `Record` 型 (または `Record`)

それぞれの特徴と使い所を詳しく見ていきましょう。

① `object` 型:プリミティブを締め出す堅実な選択

小文字の `object` は、「プリミティブ値(文字列や数値、真偽値など)を一切許容せず、オブジェクトだけを許可する」 という、私たちが直感的に期待する挙動をします。

const validObj: object = { id: 1 }; // OK!
const invalidStr: object = “hello”; // ❌ エラー!文字列はダメです

function processUser(user: object) {
// 相変わらず user.id にはアクセスできませんが、少なくとも変な値は弾けます
console.log(user);
}

「とりあえず配列やプリミティブ以外のオブジェクト全般を受け入れたい!」というときは、 `{}` ではなく `object` を使いましょう。

② `Record` 型:プロパティにアクセスしたいときの決定版

「任意のプロパティを持つオブジェクトを受け取り、さらにその中身を安全に扱いたい」という場合は、ジェネクスを活用した `Record` 型の出番です。

// 「キーが文字列で、値が何が入るか分からない(unknown)オブジェクト」を定義
function printId(user: Record) {
// 安全にプロパティを参照するには、型ガードを通します
if (user && typeof user === ‘object’ && ‘id’ in user) {
console.log(user.id); // OK!
}
}

printId({ id: 100, name: “Taro” }); // 完璧に動作します!

`Record` を使えば、何でもかんでも通してしまう `{}` の緩さを完全に排除しつつ、柔軟なオブジェクト構造を表現できます。

—

まとめ:今日の極意

ここまでのポイントをサクッと振り返りましょう!

  • `{}` は罠! `null` と `undefined` 以外のすべて(数値や文字列も含む)を受け入れてしまうので、オブジェクトのつもりで使ってはいけません。
  • オブジェクトだけを弾きたいなら `object` 型を使おう(プリミティブをシャットアウトできます)。
  • プロパティを持つ一般的な辞書型を作りたいなら `Record` を使おう。

TypeScriptの型は、私たちのコードを守るための「最強の防壁」です。その防壁の隙間を作る `{}` の性質を正しく理解して、今日から安全で美しいコードを書いていきましょう!

ここをクリアできれば、あなたのTypeScript力は確実に一段階レベルアップしていますよ。バッチリマスターしていきましょう!

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