コードレビューをしていると、今でもよく見かける光景がある。
// よくある「やらかし」コード
const results = []; // 推論結果: any[]
results.push(fetchUserData()); // ここで何でも入るガバガバな配列が誕生する
「空の配列を定義して、あとからループでデータを詰め込む」。プログラミングの基礎のようで見慣れたこのコードだが、TypeScriptのコードベースにおいて、これは型安全性のシールドを自らブチ破る行為に他ならない。
なぜ実務において `const items = []` という記述が地雷となり得るのか。コンパイラが裏側で下している残酷な決定と、それを美しく回避するプロの設計パターンを徹底的に紐解いていこう。
—
1. なぜ空配列 `[]` は `any[]`(または `never[]`)と推論されるのか?
TypeScriptのコンパイラ(tsc)がコードを評価する際、変数の初期化時にその型を決定する。もしあなたが以下のように書いたとしよう。
const users = [];
この瞬間、TypeScriptの推論エンジンはこう考える。
「この配列は現時点で要素を持たない。将来何が入ってくるか、この構文からは1ビットたりとも情報がない。さあ、どうする?」
ここでTypeScriptは、厳格モード(`strictNullChecks: true` など)のコンテキストであっても、最も「安全に後から代入できる」ようにするため、あるいは型が定まらない絶望的な状態として、初期状態の `[]` を `any[]`(または設定や文脈によっては `never[]` からの拡大変動)として推論する。
`any[]` に落ちた瞬間、TypeScriptの静的型検査は沈黙する。
- 存在しないプロパティにアクセスしてもコンパイルエラーにならない。
- 意図しない型(例えば `number` を入れたいのに `string`)を `push`しても誰も怒ってくれない。
TypeScriptを採用している意味が完全に消え去る瞬間である。
—
2. 実務で即座に使える! 堅牢な配列初期化の3つのパターン
では、APIレスポンスの集約や、動的な要素のフィルタリングなど、「どうしても空から始めざるを得ない」場面ではどう書くべきか。
プロダクションコードで採用すべき、美しく堅牢な3つのパターンを伝授する。
パターンA:明示的な型注釈(最もシンプルかつ確実)
もっともプリミティブだが、最も確実なアプローチは、変数宣言時に明示的に型をアンカー(固定)することだ。
interface User {
id: string;
name: string;
role: ‘admin’ | ‘editor’ | ‘viewer’;
}
// ❌ 悪夢の始まり
// const activeUsers = [];
// ✨ 模範解答:型注釈で最初から型を縛る
const activeUsers: User[] = [];
// 万が一、型違いのオブジェクトを入れようものなら即座にコンパイルエラー
// activeUsers.push({ id: 123, name: ‘Taro’ }); // Error: Type ‘number’ is not assignable to type ‘string’.
「型推論に頼りすぎない」ことも、時には優れたアーキテクトの選択だ。型が最初から決まっているなら、人間が明示的に宣言する方がコードの意図がドキュメント化されて読みやすくなる。
パターンB:ジェネリクスを活用したファクトリー・ヘルパー
もしあなたが共通のユーティリティ関数や、複雑な状態管理のロジックを書いているなら、型引数(ジェネリクス)を使った初期化を設計しよう。
/
- 意図された型を持つ空の配列を安全に生成するファクトリ関数
/
function createTypedArray
return [];
}
// 呼び出し側
const errorLogs = createTypedArray
// errorLogs は string[] として完璧に推論される
errorLogs.push(‘Connection timeout’);
// errorLogs.push(404); // ❌ コンパイルエラー!
このアプローチの利点は、関数スコープやクラスのプロパティ初期化時において、型アサーション(`as string[]`)という「コンパイラに対する嘘(暴力)」をつかずに、純粋なジェネリクスの型推論の恩恵を受けられる点にある。
パターンC:【最重要】`Array.prototype.map()` や `filter()` による「宣言的アプローチ」
実務の現場で `let` や `push` を使って配列を構築しているコードを見かけたら、それは命令型プログラミングの悪癖だと言っていい。
TypeScriptの型システムは、「イミュータブル(不変)なデータフロー」の上で最も美しく機能するように設計されている。
空の配列を用意して `push` する代わりに、最初からデータを変換・生成するメソッドチェインを使おう。
interface RawApiResponse {
id: number;
username: string;
is_active: boolean;
}
interface SanitizedUser {
id: string;
name: string;
}
/
- プロダクション品質のデータ変換パイプライン
- 外部APIからの入力を、pushを使うことなく安全に型付き配列へ変換する
/
async function fetchAndSanitizeUsers(apiEndpoint: string): Promise
const response = await fetch(apiEndpoint);
const rawData: RawApiResponse[] = await response.json();
// ❌ 悪い例:空配列を作ってpushする
// const results: SanitizedUser[] = [];
// for (const raw of rawData) {
// if (raw.is_active) {
// results.push({ id: String(raw.id), name: raw.username });
// }
// }
// return results;
// ✨ 模範解答:map と filter を駆使した宣言的パイプライン
// 変数は常に const で宣言され、型推論は最初から最後まで完全に維持される
const sanitizedUsers: SanitizedUser[] = rawData
.filter((raw) => raw.is_active)
.map((raw) => ({
id: String(raw.id),
name: raw.username,
}));
return sanitizedUsers;
}
このコードでは、`push` は一切登場しない。すべての変数は `const` であり、途中で状態が書き換えられる心配(副作用)がないため、保守性が飛躍的に向上する。
—
3. アーキテクトからの警鐘:型アサーション(`as`)の乱用について
最後に、よくあるアンチパターンについて言及しておこう。
「`any[]` になるなら、`as` でキャストすればいいや」と考えるエンジニアがいる。
// ❌ 絶対にやってはいけないアンチパターン
const items = [] as User[];
一見すると問題なく動くように見えるが、これはコンパイラに対する「私は正しいから、中身の検証をサボってくれ」という免罪符にすぎない。もしこの配列の初期化と実際の使用箇所の間に何百行ものコードがあった場合、予期せぬタイミングで不整合なオブジェクトが紛り込むリスクを隠蔽してしまう。
型アサーションは、TypeScriptの型システムをバイパスする「最後の切り札」だ。配列の初期化という日常的なユースケースで使うべきではない。
—
まとめ:今日のコードレビューから変えるべきこと
1. `const items = []` という記述を見つけたらリジェクトする。
2. 配列は可能な限り、`map` や `filter`、`reduce` などの宣言的APIを用いて一撃で構築・初期化する。
3. どうしても逐次追加が必要な場合は、明示的な型注釈(`const items: User[] = []`)を付与し、型安全性のリースを絶やさない。
TypeScriptを本当に「使いこなす」とは、コンパイラの機嫌を損ねることなく、その型推論エンジンを最大限にハックし、バグが入り込む余地のない美しい構造をコードベースに定着させることだ。
あなたの書くその一行の配列初期化が、チームの未来のバグを何十時間分も救うことになる。