【実務・中級編】TypeScriptにおける「空オブジェクト型 {}」が実質的にanyとして振る舞う理由 – TypeScript コア・型システムの基礎解析バイブル

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

App Header

;
};

// 呼び出し側
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 = IdleState | LoadingState | SuccessState;

// 運用例
const userState: FetchState<{ id: string; name: string }> = {
status: ‘IDLE’,
// data: { id: ‘1’ }
// ❌ ここで余計なプロパティを入れるとコンパイルエラーになり、型と実態の乖離を防げる
data: {}
};

—

4. チーフアーキテクトからの提言

TypeScriptの型システムは、コードの「意図」をコンパイラに正確に翻訳するための契約書だ。

  • `{}` を見たら、それはバグのシグナルだと思え。
  • オブジェクトの構造を絞り込みたいなら `Record` や具体的なインターフェースを使え。
  • 「何でもいい」を表現したい時は `unknown` を使い、型ガードで安全性を担保しろ。

言語の奥底にある仕様の機微を理解し、チーム全体のコード品質をネクストレベルへと引き上げてほしい。あなたの書くコードが、次の世代の堅牢なプロダクトの土台となる。

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