こんにちは。TypeScriptの世界へようこそ。
今日は、TypeScriptを学ぶ上で誰もが一度は「おや?」と立ち止まる、しかしここを乗り越えると一気に視界が開ける「関数の引数におけるタプル型の展開」についてお話ししましょう。
JavaScriptを触ってきた方なら、スプレッド演算子(`…`)には馴染みがあるかもしれません。しかし、TypeScriptにおけるスプレッド演算子は、単なる「展開」以上の意味を持ちます。それは、「型という不確定な未来を、確定した実行へと繋ぐ架け橋」なのです。
「ここをクリアすれば、TypeScriptの基本はバッチリマスターできますよ」という気持ちを込めて、その本質を紐解いていきましょう。
—
1. 配列とタプル、似て非なるものの正体
まず、基本のキから整理しましょう。TypeScriptにおいて、複数の値をまとめて扱う「配列」と「タプル」は、似ているようでいて、コンパイラから見ると全くの別物です。
- 配列 (`string[]`): 中身はすべて同じ型。長さは自由。
- タプル (`[string, number]`): どの位置に何の型があるか、そして長さが何文字かまで厳格に決まっている。
この「タプル」が持つ「厳格な長さと順序」という性質が、関数の引数と組み合わさった時に魔法を起こします。
—
2. スプレッド演算子による「引数の型推論」
例えば、名前と年齢を受け取る関数があるとします。
function displayProfile(name: string, age: number) {
console.log(`Name: ${name}, Age: ${age}`);
}
通常、この関数を呼ぶときは `displayProfile(“Alice”, 25)` と書きますよね。では、引数に渡したいデータが「タプル」として手元にある場合はどうでしょう?
タプルを展開して渡す(Calling with Spreads)
// 1. タプル型を定義
const userData: [string, number] = [“Alice”, 25];
// 2. スプレッド演算子で展開して渡す
displayProfile(…userData);
ここでのコンパイラの動きを想像してみてください。
TypeScriptは `userData` が `[string, number]` であることを知っています。そして `displayProfile` が `(string, number)` を求めていることも知っています。
スプレッド演算子を使うことで、コンパイラは「タプルの1番目の要素は関数の1番目の引数へ、2番目は2番目へ……」と、パズルのピースを嵌めるように型を検証してくれるのです。
もし `userData` が単なる `(string | number)[]`(配列)だった場合、コンパイラは「長さが足りないかもしれないし、順番が逆かもしれない!」と不安になってエラーを出します。タプルだからこそ、この型安全な展開が許されるのです。
—
3. 可変長引数(Rest Parameters)をタプルで定義する
次は逆のパターン、つまり「関数側で引数をタプルとして受け取る」方法を見てみましょう。これができると、関数の柔軟性が劇的に向上します。
/
- ユーザー情報を登録する関数
- 引数の構造をタプルで定義することで、ラベル付きで可読性を高めることができます
/
function registerUser(…info: [id: number, name: string, email: string]) {
const [id, name, email] = info;
console.log(`ID: ${id} の ${name} さん(${email})を登録しました。`);
}
// 呼び出し方は普通の関数と同じ
registerUser(1, “田中太郎”, “tanaka@example.com”);
// エラー例:型や個数が違うとコンパイラが守ってくれる
// registerUser(1, “田中太郎”); // Error: 3個の引数が必要ですが、2個指定されました。
ここがポイント!
関数の引数部分に `…info: [T1, T2]` と書くことで、TypeScriptは「複数の引数を一つのタプルとしてひとまとめに扱う」ようになります。
これにより、実行時には個別の引数として扱いながら、型定義の上では「ひとまとまりの構造」として再利用や制限が可能になるのです。
—
4. 現場で役立つ「ジェネリクス」との高度な連携
少しだけ応用編に足を踏み入れてみましょう。
「ある関数をラップして、その引数をそのまま引き継ぐ」ような高階関数を作る場合、このタプルの展開は最強の武器になります。
/
- どんな関数でも実行前にログを出すラッパー関数
/
function withLogging
fn: (…args: T) => R, // 任意の引数リスト T を持つ関数
…args: T // その関数に対応する引数のタプル T
): R {
console.log(“— 実行開始 —“);
const result = fn(…args); // ここでタプルを展開!
console.log(“— 実行終了 —“);
return result;
}
const add = (a: number, b: number) => a + b;
// 型推論により、aとbがnumberであることが自動的に導かれます
const sum = withLogging(add, 10, 20);
console.log(`結果: ${sum}`);
なぜこれが凄いの?
`T extends any[]` という部分は、「引数のリストをタプル型として丸ごとキャプチャする」という意味です。
これにより、`add` 関数の引数が `(number, number)` であることを TypeScript が自動的に推論(型推論)し、`withLogging` の後半の引数にもその制約を適用してくれます。
もはや、引数の型を一つずつ手書きする必要はありません。TypeScriptがコードの文脈から「正解」を導き出してくれるのです。
—
5. 陥りやすい文法エラーと対策
初心者が一番ハマりやすいのは、「配列をタプルだと思い込んで展開しようとする」ケースです。
const point = [10, 20]; // 型推論では number[] になる(長さ不定)
function draw(x: number, y: number) { / … / }
// @ts-expect-error: 配列は長さが不明なので、2つの引数に展開できる保証がない
draw(…point);
【解決策】
値を定義する際に `as const` をつけるか、明示的にタプル型を宣言しましょう。
const point = [10, 20] as const; // readonly [10, 20] という「固定長」として扱われる
draw(…point); // OK!
—
終わりに:TypeScriptを掌握する一歩
タプルとスプレッド演算子の組み合わせは、一見すると少し複雑に見えるかもしれません。しかし、その本質は「バラバラの引数を、一つの構造体として秩序立てる」ことにあります。
1. タプルは、厳格な「設計図」。
2. スプレッド(`…`)は、その設計図を「現場の形(引数)」に広げる作業。
この感覚を掴めれば、TypeScriptの型システムが単なる「縛り」ではなく、あなたのコーディングを助ける「最強のパートナー」に変わるはずです。
「ここをクリアすれば、TypeScriptの基本はバッチリマスターできますよ」
自信を持って、次のコードを書いてみてください。あなたのプログラムは、昨日よりもずっと堅牢で、美しいものになっているはずですから。