TypeScriptを掌握する極限の知見:なぜ `{}` は「空」ではないのか? 実務で即死する罠と正しい型選択の極意
コードレビューをしていて、もっとも背筋が凍る瞬間のひとつがこれだ。
// 一見、何も入らない安全なプレースホルダーに見えるが……
const config: {} = fetchConfig();
config.foo = “bar”; // 💥 コンパイルが通る!一体なぜ!?
TypeScript初学者から、長年JavaScriptを書いてきたシニアエンジニアまで、多くの開発者がこの罠に足を踏み入れる。
型定義における `{}`(空オブジェクト型)は、名前の裏腹に「nullとundefinedを除くすべてのプリミティブ値とオブジェクトを受け入れるTopに近い型」として振る舞う。
今日のコードレビューでは、この `{}` が孕むコンパイラ上の仕様の罠を解剖し、フロントエンドやAPI連携の現場で私たちがどのように型を制圧すべきか、その決定版を伝授しよう。
—
1. なぜ `{}` は実質的に `any` のように振る舞うのか?
TypeScriptの型システムにおいて、すべての型は「どの値の集合(Domain)を許容するか」という集合論で成り立っている。
`{}` 型が持つプロパティの定義は 「メンバーを持たないこと」 ではなく、「プロパティアクセスを禁止するものではない」 という極めて曖昧な定義だ。コンパイラは `{}` を評価する際、「`null` と `undefined` 以外の、`toString()` などの Object プロトタイプチェーンを持つすべての値」と解釈する。
結果として、以下のような悪夢のような挙動を引き起こす。
const value: {} = “Hello TypeScript”; // プリミティブなのに通る!
const num: {} = 42; // 数値でも通る!
const obj: {} = { a: 1 }; // 当然通る
// しかし、コンパイラはプロパティアクセスの安全性を保証してくれない
console.log(value.toLowerCase());
// ❌ 2000世代前のエラー: Property ‘toLowerCase’ does not exist on type ‘{}’.
「値は何でも入れられるのに、プロパティを触ろうとすると型エラーになる」。これは `any` の亜種、あるいはよりタチの悪い「安全性を剥ぎ取られた緩慢な死」に他ならない。
—
2. 代替手段の比較:`object` vs `Record`
では、私たちが本当に「オブジェクトであること」や「空であること」を保証したい場合、どの型を選択すべきか。実務で頻出する3つのパターンを比較する。
| 型 | 許容される値 | プロパティ代入・アクセス | 用途 |
| :— | :— | :— | :— |
| `{}` | `null` / `undefined` 以外のほぼ全て | 不可(型エラー) | 使用禁止(特別な理由がない限り避ける) |
| `object` | 非プリミティブ(オブジェクト、関数など) | 不可(型エラー) | 「プリミティブではない何か」を縛る時 |
| `Record
`object` 型の正しい使い方
「配列やオブジェクトなどの参照型であってほしいが、中身の構造は問わない」という境界条件で使う。
function freezeDeep(target: object) {
Object.freeze(target);
}
freezeDeep({ a: 1 }); // OK
freezeDeep([1, 2, 3]); // OK(配列はオブジェクトの一種)
freezeDeep(“string”); // ❌ プリミティブなのでコンパイルエラー
—
3. 実務で即戦力となるプロダクションコード設計
ここからが本題だ。実際のフロントエンド開発、特に「Propsを持たないコンポーネント」や「初期状態が空のAPIペイロード」を設計する際の実践的なパターンを見ていこう。
パターンA: Propsを持たないReactコンポーネント
よくあるアンチパターンとして、Props型に `{}` を指定してしまうケースがある。これでは親から不要なpropが渡されたときに型で弾けない。
import React from ‘react’;
// ❌ 悪夢のパターン:{} を指定すると、予期せぬ props がすり抜ける可能性がある
type BadHeaderProps = {};
// ✅ 模範解答:何も受け付けないことを型レベルで厳格に担保する
type StrictHeaderProps = Record
export const Header: React.FC
return
;
};
// 呼び出し側
const App = () => {
//
// ❌ ちゃんとコンパイルエラーになり、不要な汚染を防げる!
return
};
パターンB: ReduxやZustandなどのステート初期値・APIリクエスト
「まだ何もデータが入っていない初期状態」を表現する場合、`{}` を使うと不完全なデータ構造のまま型アサーションでごり押しするバグの温床になる。
// ─── 堅牢なAPIフェッチ状態の型設計 ───
// まだロードもフェッチもしていない初期状態
type IdleState = {
status: ‘IDLE’;
data: Record
};
// ローディング中
type LoadingState = {
status: ‘LOADING’;
data: Record
};
// 成功時(具体的なドメイン型を持つ)
type SuccessState
status: ‘SUCCESS’;
data: T;
};
type FetchState
// 運用例
const userState: FetchState<{ id: string; name: string }> = {
status: ‘IDLE’,
// data: { id: ‘1’ }
// ❌ ここで余計なプロパティを入れるとコンパイルエラーになり、型と実態の乖離を防げる
data: {}
};
—
4. チーフアーキテクトからの提言
TypeScriptの型システムは、コードの「意図」をコンパイラに正確に翻訳するための契約書だ。
- `{}` を見たら、それはバグのシグナルだと思え。
- オブジェクトの構造を絞り込みたいなら `Record
` や具体的なインターフェースを使え。 - 「何でもいい」を表現したい時は `unknown` を使い、型ガードで安全性を担保しろ。
言語の奥底にある仕様の機微を理解し、チーム全体のコード品質をネクストレベルへと引き上げてほしい。あなたの書くコードが、次の世代の堅牢なプロダクトの土台となる。