コードレビューをしていて、最も背筋が凍る瞬間の一つが、見慣れた顔をしたこの記号を見たときだ。
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
;
}
// 呼び出し側での悲劇
「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
// ⭕️ 正しいアプローチ 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
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を制す。今日から君のコードベースから無駄な `{}` を駆逐し、より堅牢で美しいプロダクションコードを築き上げてほしい。