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

コードレビューをしていて、最も背筋が凍る瞬間の一つが、見慣れた顔をしたこの記号を見たときだ。

const user: {} = {};

TypeScript初心者から、あまつさえ中級者クラスの開発者まで、「何でも入れられる便利な箱」「オブジェクト型を広く受け入れる型」という誤った認識のもと、この `{}`(空オブジェクト型)をコードベースのあちこちに散りばめている。

結論から言おう。TypeScriptにおいて、`{}` 型は「何でも入るオブジェクト」ではない。
それは `null` と `undefined` 以外の「ほぼすべてのプリミティブ値を含む、破壊的なトップ型(Top Type)」なのだ。

今回は、この `{}` 型がコンパイラ内部でどう評価され、実務のフロントエンド開発やAPI連携においていかに致命的なバグを引き起こすのか。そして、それをどう防ぎ、どう設計すべきかを、チーフアーキテクトの視点からロジカルかつシャープに伝授しよう。

—

1. コンパイラは `{}` をどう見ているか?(型評価の真実)

まず、TypeScriptの型システムにおける `{}`, `object`, `unknown` の違いを正確に理解しなければならない。

多くの人は、`{}` を「プロパティを持たない空のオブジェクト」だと思っている。しかし、TypeScriptの型システム(構造的型付け / Structural Subtyping)において、型とは「その値が満たすべき振る舞い(プロパティのセット)」の制約を指す。

`{}` が要求する制約は、「プロパティを持っていること」ではなく、「プロパティアクセスしたときにエラーにならないこと」(厳密には `null` / `undefined` のプロパティアクセスエラーを防げること)だ。

そのため、以下のコードを見てほしい。信じられないことに、これらはすべてエラーなくコンパイルを通過する。

const a: {} = “Hello TypeScript”; // プリミティブ(文字列)が通る!
const b: {} = 42; // 数値も通る!
const c: {} = true; // 真偽値も通る!
const d: {} = []; // 配列も通る!
const e: {} = () => {}; // 関数も通る!

// 唯一、門前払いされるのはこの2つだけ
// const f: {} = null; // Error: Type ‘null’ is not assignable to type ‘{}’.
// const g: {} = undefined; // Error: Type ‘undefined’ is not assignable to type ‘{}’.

なぜこんなことが起きるのか?

JavaScriptのランタイムでは、文字列や数値などのプリミティブ値に対しても、一時的にラッパーオブジェクトにボクシング(Box)されることで、`.toString()` などのメソッド呼び出しが可能になる。TypeScriptの `{}` は、この「オブジェクトとしての振る舞いができる(あるいは `null`/`undefined` ではない)」という極めて緩い条件を表現している。

結果として、`{}` は `null` と `undefined` を除いた実質的なトップ型(`unknown` に近い挙動をするが、プリミティブを受け入れるという歪な型)として機能してしまうのだ。

—

2. 実務の現場で起こる「最悪のバグシナリオ」

では、これがフロントエンド開発やAPI連携の現場でどう牙を剥くか。よくあるReactのコンポーネントプロパティや、APIレスポンスの型定義を想像してほしい。

// ❌ やってはいけないアンチパターン
type ComponentProps = {
payload: {}; // 「任意のオブジェクトを受け入れたい」という意図で書かれた
};

function UserProfile({ payload }: ComponentProps) {
// payload を安全にオブジェクトとして扱いたいが…
return

{JSON.stringify(payload)}

;
}

// 呼び出し側での悲劇

// 数値も渡せてしまう!

「TypeScriptを使っているのに、コンポーネントに文字列や数値が紛れ込み、ランタイムで予期せぬ挙動や予期せぬ再レンダリング、最悪の場合は画面がクラッシュする」という事故が、まさにこの `{}` 型のせいで起きる。型安全の盾を自分で投げ捨てているようなものだ。

—

3. 正しい設計:どう型を定義すべきか?

「任意のオブジェクト」を厳密に表現したい場合、ユースケースに応じて以下の型を使い分けるのがプロの作法だ。

パターンA: 辞書型(Record)としてキー・バリューを受け入れたい場合

キーが文字列で、バリューが任意の型(あるいは特定の型)を持つオブジェクトを表現したいなら、`Record` を使え。

// ⭕️ 正しいアプローチ 1
type StrictRecord = Record;

const data: StrictRecord = { id: 1, name: “Taro” }; // OK
// const invalid: StrictRecord = “string”; // Error: 型 ‘string’ は型 ‘Record‘ に割り当てられません。

パターンB: プロパティを持たない「真の空オブジェクト」を強制したい場合

もし、本当に「何もプロパティを持たないオブジェクト(あるいはそれ以上拡張させないオブジェクト)」を表現したい場合は、残念ながら素の `{}` では不十分である。厳密な制御が必要な場合はブランド型やユーティリティ型を駆使するが、通常は `Record` を用いるのが現代TypeScriptにおけるベストプラクティスだ。

// ⭕️ 正しいアプローチ 2: プロパティの追加を一切許容しない空オブジェクト
type EmptyObject = Record;

const empty: EmptyObject = {}; // OK
// const notEmpty: EmptyObject = { a: 1 }; // Error: 型 ‘{ a: number; }’ を型 ‘Record‘ に割り当てることはできません。

—

4. プロダクションコードで実践する堅牢なAPI設計

実際の非同期API連携とコンポーネント設計を組み合わせた、保守性の高いコード例を提示しよう。ここでは、型安全なAPIクライアントのレスポンスハンドリングを例にとる。

/

  • 堅牢なAPIレスポンスの基底型
  • 任意の構造を持つメタデータやペイロードを安全に取り扱う

/
export type ApiResponse = Record> = {
readonly status: number;
readonly message: string;
readonly data: TData;
};

// ユーザー情報の厳密な型定義
type UserProfileData = {
readonly id: string;
readonly name: string;
readonly email: string;
};

/

  • ユーザー詳細を取得する関数
  • 戻り値の型が確実にオブジェクトであることを保証する

/
export async function fetchUserProfile(userId: string): Promise> {
// 実際のフェッチ処理(モック)
return {
status: 200,
message: “Success”,
data: {
id: userId,
name: “Kouhei Takagi”,
email: “architect@example.com”,
},
};
}

// — 使用例 —
async function bootstrap() {
const response = await fetchUserProfile(“usr_001”);

// コンパイラが data がオブジェクトであることを保証しているため、
// 安全にプロパティへアクセスできる
console.log(response.data.name);

// 万が一、APIの設計ミスでプリミティブが返ってきた場合、
// 呼び出し側ではなく型定義の段階(またはコンパイル時)で確実に検知できる
}

—

チーフアーキテクトからの最終提言

TypeScriptの型システムは、君たちのコードの意図を正確にコンパイラに伝えるための「契約」だ。

「とりあえず何でも入るから `{}` にしておこう」という妥協は、将来の自分、そしてチームメンバーへの技術的負債の先送りに他ならない。コードレビューで `{}` を見かけたら、今日の話を思い出してほしい。

「その `{}`、本当に受け取るべきはプリミティブですか? それともオブジェクトですか?」

型を制する者が、TypeScriptを制す。今日から君のコードベースから無駄な `{}` を駆逐し、より堅牢で美しいプロダクションコードを築き上げてほしい。

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