コードレビューをしていて、最も背筋が凍る瞬間の一つが、何気なく書かれた次のような変数宣言だ。
// 一見、何の変哲もない初期化コード
const users = [];
この瞬間、TypeScriptのコンパイラ内部では何が起きているか。そして、あなたの書いたコードの型安全性は、この瞬間に音を立てて崩れ去っている。
フロントエンド開発、複雑な状態管理、あるいは堅牢であるべきAPIクライアントのコードベースにおいて、この「空配列の罠」を見過ごすシニアエンジニアはいない。今日は、なぜ空配列が `any[]`(あるいはそれに準ずる緩い型)へと堕ち、それが実務でどのような致命傷を生むのか。そして、それをどうねじ伏せるべきかについて、コンパイラの挙動から逆算した実戦的な知見を授けよう。
—
1. コンパイラはなぜ `[]` を `any[]` と推論するのか
TypeScriptにおける型推論の基本原則は、「初期化子の値から最も適切な型をボトムアップで導出する」ことだ。
しかし、考えてみてほしい。ファイル内のとある行で `const items = [];` と書かれたとき、コンパイラはその配列に「将来何が入ってくるのか」を予知するエスパー能力を持っていない。空のブラケット `[]` は、構造的に何も情報を持っていない「無」なのだ。
TypeScriptの型システムには、型が決定できない場合のフォールバック機構が存在する。`noImplicitAny` コンパイラオプションが有効な現代の厳格な環境であっても、文脈のない空配列の初期化は、過渡的に広範な型、すなわち `any[]`(または `never[]` からの拡大変動) として推論される。
これが何を意味するか。
`any[]` が宣言された瞬間、その配列に対する操作のTypeScriptによる型チェックは実質的に無効化(オプトアウト)される。
const ids = []; // 型は any[] と推論される(または暗黙のany補正を受ける)
ids.push(“user_123”); // 字符串が入る
ids.push(42); // 数値も入る
ids.push({ id: 1 }); // オブジェクトすら入る
// compiler は何も怒らない。なぜなら ids は any[] だからだ。
const first = ids[0].toUpperCase(); // 実行時エラーの爆弾がここに埋め込まれる
「私はすぐに型定義を書くから大丈夫だ」と思ったなら甘い。この `any[]` が関数の引数経由で伝播したり、ReduxやZustand、あるいはReactの `useState` の初期値としてバイパスされたりしたとき、型推論のドミノ倒しが始まり、コードベース全体が `any` の汚染を受けることになる。
—
2. 実務で遭遇する「最悪のシナリオ」:APIレスポンスとReducer
実務の現場でこれがどう牙をむくか、典型例を見てみよう。非同期APIから取得したデータを集約するReducerや、カスタムフックの内部状態を想像してほしい。
type Transaction = {
id: string;
amount: number;
status: ‘SUCCESS’ | ‘PENDING’ | ‘FAILED’;
};
// ❌ 悪手:型安全性を放棄した初期化
function useTransactionHistory() {
const [history, setHistory] = useState([]); // 推論結果: useState
const fetchMore = async () => {
const res = await api.getTransactions();
// ここで history の型が曖昧なため、誤った型アサーションやバグの温床になる
setHistory((prev) => […prev, …res.data]);
};
return { history, fetchMore };
}
このコードの恐ろしいところは、コンパイルエラーが起きないにもかかわらず、`history` の実態が意図しない型に固定化され、後続のコンポーネント側(UI層)でプロパティアクセスエラーや予期せぬ描画バグを引き起こす点だ。
—
3. 型安全な初期化のためのベストプラクティス
では、我々テクニカルリードはどう書くべきか。答えは明快である。「コンパイラに推論させず、最初から型を明示する」 もしくは 「イミュータブルな確定型に固定する」 ことだ。
パターンA:型注釈(Type Annotation)を明示する
最もプリミティブかつ確実な方法。空配列を代入する前に、変数に厳格な型を与える。
// ⭕️ 正解:最初からドメインモデルの配列であることを宣言する
const transactions: Transaction[] = [];
// これにより、誤った型の要素の混入はコンパイルエラーで即座に弾かれる
transactions.push({
id: “tx_01”,
amount: 1000,
status: “SUCCESS”
});
// ❌ コンパイルエラー: Type ‘number’ is not assignable to type ‘Transaction’.
transactions.push(42);
パターンB:ジェネリクスを活用した状態管理(React等)
Reactの `useState` などの汎用的な関数では、型引数を明示的に渡すのがプロの作法だ。
// ⭕️ 期待通りの型安全なフックの状態定義
const [history, setHistory] = useState
パターンC:`as const` によるイミュータブルな定数化(タプルとしての固定)
もしその配列が動的に変化せず、リテラル値の固定リスト(設定値やバリデーションの選択肢など)であるならば、`as const` を用いて widen(型の広がり)を防ぐべきだ。
// ⭕️ 厳密なリテラル型のreadonly配列として固定
const ALLOWED_ROLES = [] as const;
// 型: readonly never[] (必要に応じて型アサーションやアトミックな定義にする)
// 実用的な例:
const SUPPORTED_CURRENCIES = [‘USD’, ‘EUR’, ‘JPY’] as const;
type Currency = typeof SUPPORTED_CURRENCIES[number];
// “USD” | “EUR” | “JPY” という極めて堅牢なユニオン型が導出される
—
4. プロダクションコードで使える「究極の設計パターン」
非同期処理でデータをアグリゲート(蓄積)していく際のデザインパターンを提示しよう。無駄な `any` を排除し、IDEの補完(IntelliSense)が完璧に機能するモダンなTypeScriptコードだ。
import { useState, useCallback } from ‘react’;
// ドメインモデル
export interface AuditLog {
readonly id: string;
readonly action: string;
readonly timestamp: number;
}
interface UseAuditLogsReturn {
readonly logs: readonly AuditLog[];
readonly appendLogs: (newLogs: AuditLog[]) => void;
readonly clearLogs: () => void;
}
/
- 堅牢に型付けされた監査ログ管理フック
/
export function useAuditLogs(): UseAuditLogsReturn {
// 常に型を担保した状態で初期化
const [logs, setLogs] = useState
const appendLogs = useCallback((newLogs: AuditLog[]) => {
setLogs((prevLogs) => {
// イミュータブル性を維持した更新
return […prevLogs, …newLogs];
});
}, []);
const clearLogs = useCallback(() => {
setLogs([]); // ここでも初期化時に推論に頼らず、型が一致している
}, []);
return {
logs,
appendLogs,
clearLogs,
};
}
この設計が美しい理由
1. `any` の完全な排除: コードベース全体で `any` や暗黙の `any[]` が入り込む隙を与えていない。
2. イミュータビリティの強制: `readonly AuditLog[]` を返すことで、UIコンポーネント層での不意のミューテーション(`logs.push(…)` など)を型レベルでコンパイルエラーにする。
3. パフォーマンスとメモリ効率: 不必要なランタイムの型チェックや、`as` キャストによる認知負荷を削減し、V8エンジン最適化の恩恵を受けやすいクリーンなAST(抽象構文木)を維持する。
—
アーキテクトからの総括
`const items = [];` というわずか12文字のコード。
これを安易に放置することは、TypeScriptという強力な防壁に自ら穴を開ける行為に等しい。
「動けばいい」という甘えを捨て、コンパイラが何を考え、どう型を評価しているのかを常に脳内でトレースせよ。型推論に頼るべき場面と、人間が明示的に型を制約すべき場面の境界線をコントロールすることこそが、プロダクションコードの品質を担保するシニアエンジニアの条件である。
今日のコードレビューから、チームメンバーの書く `[]` の手元を厳しく見つめ直してほしい。あなたのアプリケーションは、もっと堅牢になれるはずだ。