スプレッド構文とは何か(型の世界でどう見えるか)
スプレッド構文 ... は、配列やオブジェクトの「中身を展開する」ための記法です。
配列なら「要素をバラして別の配列に入れ直す」、オブジェクトなら「プロパティをコピーする」イメージです。
const a = [1, 2, 3];
const b = [...a, 4, 5]; // [1, 2, 3, 4, 5]
TypeScriptJavaScript的には「展開」ですが、TypeScript的には「展開した結果の型をどう推論するか」がポイントになります。
配列に対するスプレッド構文と型推論
単純な配列のコピー・結合
const a = [1, 2, 3]; // number[]
const b = [4, 5]; // number[]
const c = [...a, ...b]; // number[]
TypeScripta も b も number[] なので、c も number[] と推論されます。
「同じ型の配列同士をスプレッドでつなぐと、同じ型の配列になる」という素直な挙動です。
異なる型を混ぜると、要素型はユニオンになります。
const a = [1, 2]; // number[]
const b = ["a", "b"]; // string[]
const c = [...a, ...b]; // (number | string)[]
TypeScriptTypeScriptは「number と string が混ざった配列」として扱います。
先頭にリテラルを足したときの型
const a = [1, 2, 3]; // number[]
const b = [0, ...a]; // number[]
TypeScript0 も number なので、b も number[] です。
ここでは特にタプルにはならず、「number の配列」として扱われます。
タプルとスプレッド構文(型が一気に面白くなるところ)
タプルをスプレッドすると「位置情報」が保たれる
const t1: [string, number] = ["Taro", 20];
const t2: [boolean] = [true];
const t3 = [...t1, ...t2];
// 型: [string, number, boolean]
TypeScriptここでは、t1 も t2 もタプルなので、
スプレッドした結果も「位置ごとの型が決まったタプル」として推論されます。
つまり、「タプル+タプル」をスプレッドでつなぐと、
「位置付きの型情報を保ったまま、長いタプルになる」ということです。
リテラル+タプルの組み合わせ
const t1: [number, number] = [10, 20];
const t2 = [0, ...t1];
// 型: [number, number, number]
TypeScriptここでも、「先頭の 0(number)+タプル [number, number]」ということで、
結果は [number, number, number] というタプルになります。
可変長タプルとスプレッド構文
型レベルのスプレッド(…T[] をタプルに埋め込む)
可変長タプルは、型の中でスプレッドを使います。
type Log = [string, ...number[]];
const a: Log = ["sum"]; // OK
const b: Log = ["sum", 1, 2, 3]; // OK
TypeScriptここで ...number[] は、「2番目以降は number が0個以上続いていい」という意味です。
「先頭だけ位置が決まっていて、後ろは配列」という構造を、型で表現しています。
実際のスプレッド構文との相性
function logSum(...args: [string, ...number[]]) {
const [label, ...values] = args;
console.log(label, values.reduce((a, b) => a + b, 0));
}
logSum("total"); // OK
logSum("total", 1, 2); // OK
TypeScriptここでは、
呼び出し側:...(実引数のスプレッド)
型側:[string, ...number[]](可変長タプル)
という形で、「スプレッド構文」と「スプレッドを含むタプル型」がきれいに対応しています。
スプレッドで「コピーしてから安全にいじる」型の感覚
readonly配列・タプルからのコピー
const xs: readonly number[] = [1, 2, 3];
const ys = [...xs]; // ys: number[]
ys.push(4); // OK(ys は普通の配列)
TypeScriptxs は readonly number[] ですが、スプレッドでコピーすると新しい number[] になります。
「元は不変のまま」「コピーした方だけ自由に変更」というスタイルが取りやすくなります。
タプルでも同じです。
const t: readonly [string, number] = ["Taro", 20];
const u = [...t]; // u: (string | number)[]
TypeScriptここでは「タプル → 普通の配列」に崩れますが、
「元を壊さずに、中身だけ取り出して別の配列として扱う」という意図ははっきりしています。
まとめの感覚:スプレッド構文は「型の形を保ったまま広げる」
スプレッド構文は、値の世界では「展開」ですが、
型の世界では「展開した結果の配列・タプルの形をどう保つか」という話になります。
配列同士なら「要素型をマージした配列」
タプル同士なら「位置情報をつなげたタプル」
可変長タプルなら「固定部分+可変部分」をそのまま表現
というふうに、「形」を意識して見ると一気に理解しやすくなります。
コードを書きながら、
「この ... は、型の世界ではどんな形に広がっているんだろう?」
と一瞬だけ立ち止まってみると、スプレッド構文と型の関係がどんどんクリアになっていきます。
