コードレビューをしていて、最も頻繁に、そして静かにプロジェクトの型安全性をむしばむアンチパターンの一つがこれだ。
// よく見る光景
const results = [];
このコードを書いた瞬間、あなたのTypeScriptコンパイラは背後でため息をついている。いや、正確にはこう評価しているのだ:「あぁ、このプログラマは、これから何が入るか分からない `any` の箱を作りたいんだな」 と。
今回は、この空配列 `[]` の型推論の罠に焦点を当て、なぜこれがコンパイラにとって「地雷」なのか、そして実務の現場でどうやって堅牢な配列初期化パターンを構築すべきかを、コンパイラの挙動から紐解いていこう。
—
1. なぜ `const arr = []` は `any[]` になるのか?
TypeScriptの型推論エンジンは、変数が初期化された瞬間にその型を決定する。しかし、何も要素が入っていない空の配列 `[]` が渡されたとき、コンパイラは「将来的にどんな型が入るのか」を未来予測することはできない。
型推論のアルゴリズムにおいて、情報が完全に欠落している場合、TypeScriptは安全装置を外し、拡大変数(Widening)によって最も柔軟(かつ危険)な型である `any[]`(あるいは `noImplicitAny` が有効な場合は一時的な暗黙の型)へとフォールバックする。
const items = []; // 推論: any[]
items.push(1); // OK (anyだから何でも入る)
items.push(“string”); // OK (型安全の崩壊の始まり)
items.push({ foo: bar }); // もはや何がしたいのか分からないゴミ箱の完成
これが意味するのは、「型安全なコードを書くためにTypeScriptを使っているのに、自ら率先して `any` の魔境へ足を踏み入れている」という矛盾だ。この `items` をそのまま関数の引数に渡したり、ReduxやZustandなどの状態管理に組み込んだりした瞬間、型チェックは形骸化する。
—
2. 「型注釈」という名の応急処置、その限界
この問題に対する最もナイーブな解決策は、明示的な型注釈(Type Annotation)をつけることだ。
// ナイーブなアプローチ
const userIds: number[] = [];
userIds.push(42); // OK
userIds.push(“42”); // ちゃんとコンパイルエラーになる
これは単なるスクリプトであれば機能する。だが、フロントエンドのコンポーネント設計や、複雑な非同期API連携(たとえば、ページネーションやフィルタリングを伴うデータストリームの集約)の現場では、これだけでは表現力が圧倒的に不足する。
特に、以下のようなケースに直面したとき、単純な `T[]` 注釈は破綻する。
- 最初は空だが、後から厳密なタプル(Tuple)構造に変化させたい場合
- APIから返ってきた未知のデータ構造を、条件分岐を経て型ガードしながら安全に詰め替えていく場合
- リードオンリー(`readonly`)な配列として厳格に扱いたい場合
—
3. 実務で即座に使える!型安全な配列生成・初期化パターン
ここからが本題だ。プロダクションコードで絶対にバグを出さない、テクニカルリード推奨の洗練されたパターンを提示する。
パターン A: ジェネリクスを用いたファクトリー関数(型推論の誘導)
もし動的に要素を追加していくタイプの配列が必要なら、単に `[]` を書くのではなく、「どのような型が入りうるか」をコンパイラに明示的に約束させるジェネリック関数を一枚挟む。
/
- 型安全に空の配列を生成するためのユーティリティ
- コンパイラに対し、この配列の意図された型を強制する
/
function createTypedArray
return [];
}
// 使用例:APIレスポンスの蓄積ロジック
interface User {
id: string;
name: string;
}
// 綺麗に User[] と推論される
const activeUsers = createTypedArray
// activeUsers.push(“hoge”); // ❌ ちゃんとコンパイルエラー!
activeUsers.push({ id: “1”, name: “Alice” }); // ⭕️
パターン B: `satisfies` 演算子と `as const` によるイミュータブルな初期化
近年のTypeScript(4.9以降)において、配列の初期化を語る上で欠かせないのが `satisfies` だ。
特に、設定値や初期状態のモックなどにおいて、型を緩めずにリテラル推論を維持したい場合に真価を発揮する。
type Role = ‘admin’ | ‘editor’ | ‘viewer’;
// 「初期状態は空だが、将来的にRole型しか入らないリードオンリー配列」を強制
const roles = [] as const satisfies readonly Role[];
// エラーになることを確認:
// roles.push(‘super-admin’);
// そもそも as const なので push が存在しない(イミュータブルの担保)
パターン C: 非同期API連携における「reduce / map」の脱・空配列アンチパターン
非同期で取得した膨大なリストデータを加工する際、よくやりがちなのがこれだ。
// ❌ アンチパターン:空配列を作ってループでpushする
async function fetchAndTransform(apiEndpoints: string[]) {
const results = []; // any[] になる地雷
for (const endpoint of apiEndpoints) {
const data = await fetch(endpoint).then(res => res.json());
results.push(transform(data));
}
return results; // 型が汚染されている
}
これをモダンで堅牢なコードにリファクタリングしよう。`Promise.all` と `map` を用いることで、そもそも「空配列の初期化とミュータブルな操作」をコードベースから駆逐する。
// ⭕️ プロダクションクオリティのコード
interface TransformedData {
id: number;
value: string;
}
async function fetchAndTransformSafe(apiEndpoints: readonly string[]): Promise
// map と Promise.all により、型推論は最初から最後まで完全に維持される
const promises = apiEndpoints.map(async (endpoint): Promise
const response = await fetch(endpoint);
const json = await response.json();
return transform(json);
});
return Promise.all(promises);
}
このアプローチには、型安全性の向上だけでなく、非同期処理の並列実行によるパフォーマンス上の絶大なメリットもある。`for` ループで直列に `push` するコードは、実行速度の観点からも最悪だ。
—
4. チーフアーキテクトからの提言
TypeScriptの型システムは、あなたのコードの「意図」をコンパイラと共有するための契約書だ。
たかが `[]`、されど `[]`。コードの最初の一行にある `const arr = []` という油断が、やがて巨大なアプリケーションの型安全性を崩壊させるドミノの最初の一片となる。
明日からのコードレビューでは、`[]` が見つかったらこう問いかけてほしい。
「その空配列の型、コンパイラに丸投げしていませんか?」
厳格な型推論を味方につけたとき、あなたの書くTypeScriptは、単なるJavaScriptのラッパーを超え、最も信頼できるドキュメントへと昇華するはずだ。