こんにちは!TypeScriptの型システムの世界へようこそ。
日々バリバリとコードを書いていると、ふと「あれ、なんでこの配列、こんな型になっちゃうんだろう?」と立ち止まる瞬間はありませんか?
今回は、多くの開発者が最初に遭遇する小さな罠でありながら、TypeScriptの型推論の核心に迫るテーマ「空配列 `[]` が `any[]`(または `never[]`)と推論される理由と、型安全な初期化の極意」について、一緒に深く掘り下げていきましょう。
ここをクリアすれば、TypeScriptの型推論の機微が手に取るようにわかるようになりますよ。バッチリマスターしていきましょう!
—
1. 最初の疑問:なぜ空配列は `any[]` になってしまうのか?
例えば、次のようなコードを書いたとします。
// 空の配列を作って、あとから要素を追加しようとしたとき…
const userIds = [];
// エディタでホバーしてみると?
// ➔ const userIds: any[]
「あれ?何も型を指定していないのに、勝手に `any[]` になっちゃったぞ?」と思ったことはありませんか?
これには、TypeScriptコンパイラが裏で行っている「型推論のタイムリミット(段階的評価)」という事情が関係しています。
コンパイラの頭の中を覗いてみよう
TypeScriptがコードを読めるとき、コンパイラは常に「この変数には将来どんな値が入るのか?」を予測(推論)しようとします。
しかし、`const userIds = [];` と書かれた瞬間、コンパイラには次のような情報しかありません。
1. 変数 `userIds` が作られた。
2. 中身は空っぽである。
中身が空っぽということは、文字列が入るのか、数値が入るのか、それともオブジェクトが入るのか、コンパイラには予測する手がかりがゼロです。
ここでTypeScriptの設計思想(JavaScriptの柔軟性を殺さないこと)が働きます。
「情報がないなら、とりあえず何でも入れられる `any`(何でもあり)にしておけばエラーにならないよね?」という親切心(あるいは妥協)から、コンパイラはこれを `any[]` と推論するのです。
—
2. `any[]` がもたらす「型安全の崩壊」というリスク
「エラーにならないなら便利じゃん!」と思ったそこのあなた、ちょっと待ってください。`any` 型を使うということは、TypeScriptの強力なセーフティネットを自らブチ破ることを意味します。
次のようなコードを見てみましょう。
const items = []; // 推論: any[]
items.push(“apple”); // 文字列を入れた
items.push(42); // 数値を入れた! TypeScriptは怒らない(anyだから)
// そして、文字列用のメソッドを呼び出してみると…?
const upper = items[0].toUpperCase();
// 💥 実行時エラー! items[0] は 42 (数値) なので toUpperCase は存在しない!
コンパイル時には一切エラーが出ないにもかかわらず、実行時に突然アプリがクラッシュするという、JavaScript時代に散々苦しめられた悪夢が復活してしまいます。これでは何のためにTypeScriptを使っているかわかりませんよね。
—
3. 型安全な初期化のためのベストプラクティス
では、空の状態から安全に配列を育てていくには、どうすればよいのでしょうか?
現場で即座に使える3つのアプローチを伝授します。
パターンA:型注釈(Type Annotation)を明示する(王道)
最もシンプルで、他の開発者にとっても意図が伝わりやすい方法です。最初から「この配列には何が入るのか」を宣言しておきます。
// 「この配列は string型 の要素だけを入れる箱です」と明示する
const userIds: string[] = [];
userIds.push(“user_001”); // OK
userIds.push(“user_002”); // OK
// userIds.push(123);
// ❌ コンパイルエラー: 型 ‘number’ の引数を型 ‘string’ のパラメーターに割り当てることはできません。
これなら、コンパイラがしっかりと監視してくれます。間違った型のデータが入り込む余地はありません。
パターンB:ユニオン型で複数の型を許容する
「最初は空だけど、数値も文字列も入る可能性があるんだよ」という場合は、ユニオン型を使いましょう。
// 数値または文字列が入る配列として初期化
const mixedValues: (string | number)[] = [];
mixedValues.push(“hello”); // OK
mixedValues.push(100); // OK
// mixedValues.push(true); // ❌ boolean はエラーになる
パターンC:`as const` や ジェネリクスで「イミュータブル(読取専用)」にする
もし配列の要素を後から `push` で追加するのではなく、初期化時あるいは関数から新しい配列として取得する場合は、`as const` や適切な型指定が力を発揮します。
(※ちなみに、近年のTypeScriptでは、型推論がより賢くなり、文脈によっては `never[]`(何も追加できない配列)として推論されることもあります。これは「もう何も追加させないよ」という安全寄りの推論です)
// 読み取り専用の空配列として固定したい場合
const emptyConfig = [] as const;
// 推論: readonly never[]
—
まとめ:今日の極意
ここまでのポイントをサクッと振り返りましょう!
1. `const x = []` と書くと、TypeScriptは親切心(と情報のなさ)から `any[]` と推論してしまう。
2. `any[]` を放置すると、型安全性が失われ、実行時エラーの温床になる。
3. 必ず初期化時に型注釈(`const list: string[] = []`)を書き、コンパイラに「守るべきルール」を教えよう。
TypeScriptの型推論は非常に強力ですが、「何も情報がないところから型を作ることはできない」という大原則があります。そこを人間(あなた)がちょっとだけ手助けしてあげることで、コードベースは驚くほど堅牢になります。
ここをクリアできれば、もう基本の型システムで迷うことはありません。自信を持って、安全で美しいコードを書いていきましょう!