こんにちは!フロントエンドからバックエンドまで、TypeScriptで頭を悩ませる日々にワクワクしているあなたへ。
今日は、多くの開発者がTypeScriptの学習初期、そして実務でもふとハマりがちな「空配列の罠」についてお話ししますね。
「えっ、ただの配列 `[]` でしょ?何が問題なの?」と思ったそこのあなた。
ここをクリアできるかどうかが、TypeScriptの型システムと仲良くなれるかどうかの大きな分かれ道なんです。
今回は、なぜ空配列が危険なのか、コンパイラの裏側で何が起きているのかを、優しく紐解いていきましょう。ここをマスターすれば、あなたの書くコードの安全性は一段と跳ね上がりますよ!
—
1. 誰もが通る道:空配列 `[]` が生む「静かなる恐怖」
まずは、よくあるこんなコードを見てみましょう。
// 空の配列を定義する
const items = [];
// 後から要素を追加していく
items.push(“apple”);
items.push(42); // あれ?数値も入っちゃった!
他の言語(例えばJavaやC#など)からやってきた方だと、「空の配列を作って、後から要素を入れていく」というのはごく自然なフローですよね。
しかし、TypeScriptでこれをやると、コンパイラは次のような型推論を下ろします。
const items: any[] = [];
そう、初期値として `[]` を渡した瞬間、TypeScriptは右側の情報から型を推論できず、「あ、中身が分からないから、何でも入れられる `any` の配列にしておこう!」と優しさ(のつもり)で解釈してしまうのです。
`any` がもたらす「型安全の崩壊」
TypeScriptにおける `any` は、「型チェック機能を一時停止する魔法のカード」です。
`items` が `any[]` になってしまうと、その後のコードでこんな恐ろしいことが起きても、TypeScriptは一切警告を出してくれません。
const users = []; // 型は any[]
// 100行下のコード…
users.push({ name: “Alice”, age: 25 });
// 200行下のコード… うっかりスペルミス!
users.push(“この文字列はバグの元”);
// 実行時エラーの爆弾を抱えたまま、何食わぬ顔でビルドが通ってしまう
users.forEach(user => {
console.log(user.name.toUpperCase()); // “this string has no property ‘name'” でクラッシュ!
});
「TypeScriptを使っているのに、JavaScriptを書いている時と変わらない恐怖がある……」と感じたら、それは大体この `any[]` の罠が原因です。
—
2. なぜ `const` で宣言しても型が狭まらないのか?
「いやいや、`const` で宣言してるんだから、定数なんだし、型も固定されるでしょ?」と思うかもしれません。
ここで、TypeScriptの型評価のメカニズムを少しだけ覗いてみましょう。
TypeScriptは、コードを解析するときに「この変数は後から書き換えられる(ミュータブルな)可能性はあるか?」を見ます。
`let` はもちろん、`const` で宣言された配列であっても、`.push()` などのメソッドを使って中身を後から変更することができてしまいますよね。
そのため、TypeScriptはこう判断します。
> 「あ、この配列は中身が空っぽの状態で生まれて、後から何が入ってくるか分からない。だから、安全のために widest(最も広い)な型である `any[]` にしておこう」
これが、コンパイラが下す冷徹かつ合理的な判断なのです。
—
3. 解決策:型安全に配列を初期化する3つのパターン
じゃあ、空から配列を始めたいときはどうすればいいの?という話ですよね。
ここからが本題です。実務で使えるスマートな解決策を3つ伝授しますね。
パターンA:型注釈(Type Annotation)を明示する(王道)
一番シンプルで確実な方法がこれです。変数を宣言する時に、「この配列には何が入るのか」を人間が明示してあげます。
// 「文字列の配列だよ」とあらかじめ教えてあげる
const names: string[] = [];
names.push(“Alice”);
names.push(“Bob”);
// names.push(123);
// ❌ コンパイルエラー!「型 ‘number’ の引数を型 ‘string’ のパラメーターに割り当てることはできません」
コンパイラに「私は将来、文字列を入れるつもりです」と最初に宣言しておくことで、`any[]` への堕落を防ぐことができます。
パターンB:最初から初期値を埋める(理想的)
もし、配列に入れる初期データがいくつか決まっているのであれば、最初から入れてしまうのがTypeScriptの恩恵を最も受けられる書き方です。
// 初期値から型が string[] と推論される
const fruits = [“apple”, “banana”, “orange”];
fruits.push(“grape”); // OK
// fruits.push(100); // ❌ コンパイルエラー!
TypeScriptの型推論は、初期値がある場合に最も強力に働きます。迷ったら「最初から中身を入れておく」が鉄則です。
パターンC:ジェネリクスを活用する(少し高度で柔軟なアプローチ)
関数の中で動的に配列を作りたい場合や、型を柔軟に切り替えたい場合は、ジェネリクス(型引数)を使うのがプロの技です。
// ジェネリクスを使って、型を外から注入できるようにする
function createArray
return [];
}
// 呼び出し時に型を決定する
const numbers = createArray
// numbers の型は number[] になる!
numbers.push(10);
// numbers.push(“hello”); // ❌ コンパイルエラー!
このように、関数やクラスのレベルで型を抽象化(ジェネリクス)しておくと、コンパイル時にしっかりと型の整合性が保たれます。
—
4. まとめ:今日から実践できること
いかがでしたでしょうか? 今回のポイントをギュッと凝縮して振り返ってみましょう。
1. `const items = []` と書くと、TypeScriptは `any[]` と推論する。
2. `any[]` は型チェックを無効化するため、バグの温床になる。
3. 対策として、明示的な型注釈 (`string[] = []`) を書くか、最初から初期値を与えよう。
「たかが空配列、されど空配列」。
この小さな `[]` の扱いに気を配るだけで、あなたの書くTypeScriptコードの堅牢性は見違えるほど向上します。
ここをクリアできれば、TypeScriptの型推論の癖がグッと掴めるようになりますよ。今日の開発から、ぜひ意識してコードを書いてみてくださいね。それでは、快適なTypeScriptライフを!