TypeScriptのコードレビューをしていて、最も背筋が凍る瞬間の一つが、見慣れたこの記号を見つけたときだ。
const payload: {} = …;
「オブジェクトを受け取るから、とりあえず `{}` にしておこう」――この軽い気持ちが、プロダクション環境で静かに、しかし確実にバグを育成する。
この記事では、TypeScriptの型システムにおける `1軍の罠` である「空オブジェクト型(`{}`)」の正体を暴き、なぜそれが危険なのか、そして現代のフロントエンド開発やAPI連携において、我々プロフェッショナルはどうコードを設計すべきかをロジカルに解説する。
—
1. なぜ `{}` は「何でも許してしまう」のか?(型システムの闇)
TypeScriptの初学者はもちろん、中級者ですら誤解しているのが、`{}` は「空のオブジェクト(プロパティを持たないオブジェクト)型」であるという直感だ。
しかし、コンパイラの内部(型チェッカー)において、`{}` が持つ意味は全く異なる。「`null` と `undefined` 以外のすべてのプリミティブ値を含む、事実上のトップ型に近い何か」である。
以下のコードを見てほしい。
// すべてコンパイルエラーにならない!
const a: {} = “hello world”; // 文字列が通る
const b: {} = 42; // 数値が通る
const c: {} = true; // 真偽値が通る
const d: {} = []; // 配列も通る
const e: {} = () => {}; // 関数も通る
// もちろん、オブジェクトも通る
const f: {} = { id: 1 };
// 例外はこれらだけ
const g: {} = null; // ❌ 厳格モードではエラー
const h: {} = undefined; // ❌ 厳格モードではエラー
なぜこんな挙動になるのか?
TypeScriptの型システムは 「構造的型付け(Structural Subtyping)」 をベースにしている。
`{}` は、「プロパティを一切要求しないオブジェクト」を意味する。JavaScriptにおいて、文字列や数値などのプリミティブ値は、実行時にはラッパーオブジェクト(`String`, `Number` など)のプロトタイプチェーンを継承しており、「プロパティを持たない要件」を構造的に満たしてしまうのだ。
そのため、`{}` は実質的に `NonNullable
—
2. 実務で遭遇する最悪のバグシナリオ
例えば、Reactのコンポーネント設計で「初期状態ではプロパティを受け取らない」つもりで以下のように書いたとする。
type Props = {};
export const UserCard: React.FC
return
;
};
// 呼び出し側でうっかり数値を渡してしまった!
あるいは、APIレスポンスのパース前データを受け取る関数:
function processPayload(data: {}) {
// data.id にアクセスしたいが、dataが文字列や数値の可能性もあるため危険
}
「オブジェクトだけを受け入れたい」という意図に対して、`{}` は全く無力である。
—
3. 代替手段の使い分け:`Record` vs `object` vs `Record`
では、我々はどう型を定義すべきなのか。要件に応じた正しい使い分けをマトリクスとして頭に叩き込んでおこう。
① 「任意のキーを持つ辞書(Dictionary)」を表現したい場合
キーが動的で、値の型が不確定、あるいは安全に扱いたい場合は `Record
// ❌ 危険な書き方
const oldConfig: {} = JSON.parse(rawString);
// ✅ 正しい書き方(unknownで受けて、型ガードで安全に絞り込む)
const safeConfig: Record
`unknown` を値に指定することで、プロパティにアクセスする際に必ず型チェック(あるいはナローイング)を強制できる。
② 「厳密にプリミティブ以外のオブジェクト(配列や関数含む)」を表現したい場合
JavaScriptの `typeof x === ‘object’` に近い挙動を型で保証したい場合は `object`(小文字)を使う。
function freezeObject(target: object) {
return Object.freeze(target);
}
freezeObject({ name: “ts” }); // ⭕️ 通る
freezeObject([1, 2, 3]); // ⭕️ 通る(配列もオブジェクト扱い)
freezeObject(“not an object”); // ❌ コンパイルエラー!
③ 「絶対にプロパティを持たない(真の空)オブジェクト」を表現したい場合
「プロパティが存在してはならない」という制約を型レベルで強制したい、極めて稀だが重要なユースケース(Reactのプロパティなしコンポーネントなど)では、以下のように書く。
type StrictEmpty = Record
const val: StrictEmpty = {}; // ⭕️ 通る
const invalid: StrictEmpty = { a: 1 }; // ❌ プロパティは許されない
const str: StrictEmpty = “test”; // ❌ プリミティブも許されない
—
4. プロダクションコードで使える:堅牢な設計パターン
最後に、実務の非同期API連携やコンポーネント設計において、これらの知見を統合した「美しいプロダクションコード例」を提示する。
import { z } from ‘zod’; // 実行時バリデーションのデファクト
/
- 1. APIからの入力境界 (Boundary)
- 不確実な外部データを安全に型付けしつつ、`{}` のような曖昧さを排除する。
/
const ApiPayloadSchema = z.record(z.string(), z.unknown());
type ApiPayload = z.infer
/
- 2. 厳密なオブジェクト処理関数
- プリミティブを絶対に受け付けない `object` 型を活用。
/
const sanitizePayload =
// 実装詳細:ディープクローンやサニタイズ処理
console.log(“Sanitizing object keys…”);
return payload;
};
/
- 3. コンポーネント設計
- Propsがないことを明確にするために、`{}` ではなく不要な定義を避けるか、
- 明示的に空であることを示す型制約を置く。
/
type AsyncBoundaryProps = {
fallback: React.ReactNode;
// プロパティを持たないことを明確にする場合
emptyConfig?: Record
};
export const SafeAsyncBoundary: React.FC
children,
fallback
}) => {
// 実行例
const rawInput: unknown = fetchUntrustedData();
// 境界でのバリデーション
const result = ApiPayloadSchema.safeParse(rawInput);
if (!result.success) {
return <>{fallback}>;
}
// object型制約のある関数へ安全に流し込む
const sanitized = sanitizePayload(result.data);
return (
{children}
);
};
function fetchUntrustedData(): unknown {
return { status: “ok”, code: 200 };
}
—
チーフアーキテクトからの提言
TypeScriptの型は、単なるドキュメントではない。「コンパイルタイムにおけるビジネスロジックの契約(Contract)」である。
`{}` という記述を見かけたら、それはサボタージュの兆候だ。「なぜここで `{}` を使っているのか? `Record
型規律の厳格さこそが、大規模フロントエンド開発をスケールさせる唯一の盾となる。