「配列に異なる型を入れる」と TypeScript はどう解釈するか
まず前提として、TypeScript は「配列の中身(要素)を見て、その配列の型を決める」言語です。
なので、配列に異なる型を混ぜると、その要素たちをまとめて表現できる型を自動的に作ろうとします。
const mixed = [1, "2", 3];
// 型: (number | string)[]
TypeScriptこの場合、mixed は「number または string の配列」と推論されます。
つまり、「要素ごとに number か string かは違ってもいいけれど、そのどちらかであることは保証される」という状態です。
ここから、「ユニオン型の配列」と「タプル」との違いを意識していくのが大事なポイントになります。
ユニオン型の配列として扱われる場合
number と string が混ざった配列
const values = [1, "2", 3];
values.push(4); // OK
values.push("5"); // OK
// values.push(true); // エラー
TypeScriptこのときの型は (number | string)[] です。
「number か string のどちらかなら入れていい」という意味なので、true のような boolean は弾かれます。
ここで重要なのは、「配列の各要素は同じ型である必要はない」ということです。
TypeScript は、「この配列は number か string のどちらかを入れる場所」と理解しているので、
取り出したときもそのように扱われます。
const v = values[0]; // v: number | string
TypeScriptv は number か string か分からないので、そのままでは数値計算も文字列メソッドも安全に呼べません。
必要なら typeof で絞り込む(型ガードする)ことになります。
ユニオン型の配列と「配列のユニオン」の違い
(number | string)[] と number[] | string[] は別物
ここは初心者がつまずきやすいポイントなので、少し丁寧にいきます。
let a: (number | string)[]; // 要素ごとに number か string
let b: number[] | string[]; // 配列そのものが number[] か string[] のどちらか
TypeScript(number | string)[] は、「1つの配列の中に number と string が混ざっていてもいい」型です。
一方 number[] | string[] は、「この変数には number の配列か string の配列のどちらかが入る」という意味で、
1つの配列の中で混ざることは想定していません。
a = [1, "2", 3]; // OK
// b = [1, "2", 3]; // NG(number[] でも string[] でもない)
b = [1, 2, 3]; // OK(number[])
b = ["a", "b"]; // OK(string[])
TypeScript「要素が混ざる」のか、「配列の種類が2パターンある」のか——
ここを意識して型を書くと、配列の設計がかなりクリアになります。
「何でも入る配列」になってしまう危険パターン
空配列+異なる型の push
よくある落とし穴がこれです。
let xs = [];
xs.push(1);
xs.push("hello");
TypeScriptこの場合、xs は any[] になりがちです。
TypeScript は最初に「中身が分からない配列」として扱い、その後も「何でも入る配列」として扱ってしまいます。
any[] になると、型チェックがほぼ効かなくなります。
xs.push(true); // これも通ってしまう
TypeScript「異なる型を入れたい」のと「何でもアリにしたい」のは別物です。
前者なら (number | string)[] のように意図を持ってユニオン型を書くべきで、
後者(何でもアリ)は基本的に避けるべきです。
「異なる型を入れたい」ときに考えるべきこと
本当に「1つの配列に混ぜる」のが正しいか?
たとえば、こんな配列を考えてみます。
const data = [1, "Taro", true];
TypeScript型としては (number | string | boolean)[] になりますが、
これを見て「この配列、何の配列?」と聞かれたときに、うまく答えられるでしょうか。
「ユーザー情報の配列」なら、素直にオブジェクトにした方が意味が伝わりやすいです。
const user = {
id: 1,
name: "Taro",
isActive: true
};
TypeScriptあるいは、複数人分なら User[] にします。
type User = {
id: number;
name: string;
isActive: boolean;
};
const users: User[] = [
{ id: 1, name: "Taro", isActive: true },
{ id: 2, name: "Hanako", isActive: false }
];
TypeScript「異なる型を1つの配列に詰め込む」のは、
「本当はオブジェクトや別の構造にした方がいいのに、無理やり配列にしている」サインであることも多いです。
それでも混ぜたいなら、ユニオン型として意図をはっきりさせる
たとえば、「ログの配列で、数値IDか文字列メッセージが入る」という仕様なら、(number | string)[] と明示してしまうのはアリです。
type LogItem = number | string;
const logs: LogItem[] = [1, "start", 2, "end"];
TypeScriptこのとき大事なのは、「この配列は LogItem の配列だ」と名前を付けてあげること。
そうすると、「何でもアリの配列」ではなく、「仕様としてこういう値が混ざる配列」だと読み手に伝わります。
タプルという選択肢もある(位置ごとに型が決まっている場合)
「1番目は string、2番目は number」のような配列
「異なる型を入れたい」と言っても、
「どの位置に何が入るかが決まっている」場合は、タプル型の方が適しています。
let user: [string, number] = ["Taro", 20];
TypeScriptこれは「string と number が混ざった配列」ではなく、
「1番目は string、2番目は number」という位置に意味がある配列です。
複数人分なら、タプルの配列にできます。
let users: [string, number][] = [
["Taro", 20],
["Hanako", 18]
];
TypeScript「順番に意味がある」「要素数が決まっている」ならタプル、
「順番に意味はなく、同じ型の値が並ぶ」なら配列、
「複数の型が混ざるならユニオン型の配列」——
このあたりの切り分けが見えてくると、設計が一気に気持ちよくなります。
まとめの感覚:「混ぜるなら、意図を型で表現する」
配列に異なる型を入れること自体は、TypeScript的には全然アリです。
ただし、そのときに大事なのは、
「何が混ざるのか」をユニオン型で明示する
「何でもアリの any[] に逃げない
「本当はオブジェクトやタプルの方が自然じゃないか?」と一度立ち止まる
という3つの視点です。
「この配列は何の配列か?」
「その“何”は、型としてどう表現できるか?」
そこまで言語化できたとき、配列はただの「値の並び」から、
あなたの設計意図をそのまま映した「型付きのデータ構造」に変わります。
