【実務・中級編】配列の初期化と型推論の罠:空配列[]がany[]と推論される理由と型安全な初期化パターン – TypeScript コア・型システムの基礎解析バイブル

コードレビューをしていて、最も頻繁に、そして静かにプロジェクトの型安全性をむしばむアンチパターンの一つがこれだ。

// よく見る光景
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(): T[] {
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のラッパーを超え、最も信頼できるドキュメントへと昇華するはずだ。

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