「可変長引数」は型の世界だとどう見えるのか
まず、可変長引数は JavaScript 的にはこうです。
function sum(...numbers: number[]) {
return numbers.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3);
sum(10, 20);
sum();
TypeScript...numbers は「引数を何個でも受け取れる」構文ですが、
TypeScript 的には「number[] という配列を1つ受け取っている」のと同じです。
つまり、
可変長引数を「型安全」にしたいというのは、
「この ...args が、どんな配列(どんなタプル)であるべきかを、
ちゃんと型として表現したい」
という話になります。
ここから、段階を上げながら「型安全な可変長引数」の設計を見ていきます。
段階1:同じ型だけをいくつでも受け取る
一番基本のパターン
「全部 number」「全部 string」のように、
同じ型だけをいくつでも受け取りたいなら、素直にこう書きます。
function sum(...numbers: number[]) {
return numbers.reduce((acc, n) => acc + n, 0);
}
TypeScriptこのときの型は、
(...numbers: number[]) => number
TypeScriptです。
ここでのポイントは、
「...numbers の型は number[] であり、
関数の中では“普通の配列”として扱う」ということです。
このレベルでは、特に難しいことはありません。
「可変長引数=配列」として素直に受け取ればOKです。
段階2:先頭だけ必須、残りは任意の数だけ受け取る
「最低1つは必要」という制約を型で表す
例えば、「最低1つの値は必須で、それ以降はあってもなくてもいい」関数。
function logFirstAndRest(first: string, ...rest: string[]) {
console.log("first:", first);
console.log("rest:", rest);
}
logFirstAndRest("A");
logFirstAndRest("A", "B", "C");
TypeScriptここでは、
first: string が必須rest: string[] が「2つ目以降の可変長」
という設計になっています。
このパターンはよく使います。
「先頭に“意味のある必須引数”、後ろに“追加情報をいくつでも”」という構造です。
ここまでなら、まだ T[] だけで十分です。
段階3:タプル型で「個数」と「位置ごとの型」を固定する
「1番目は string、2番目は number、3つ目以降は number」のようなパターン
ここからが「型安全化」の本番です。
例えば、こういう関数を考えます。
「最初の2つは必須の number、それ以降は任意の number」
function handlePairAndMore(...values: [number, number, ...number[]]) {
const [first, second, ...rest] = values;
console.log("first:", first);
console.log("second:", second);
console.log("rest:", rest);
}
handlePairAndMore(1, 2);
handlePairAndMore(1, 2, 3, 4);
// handlePairAndMore(1); // エラー:最低2つ必要
TypeScript[number, number, ...number[]] はタプル型です。
「最初の2つは必ず number、その後に任意個の number が続く配列」
という形を、型として表現しています。
ここでの重要ポイントは、
「rest引数の型は、単なる T[] だけでなく、
“タプル+rest” という形で“最低限必要な個数”まで表現できる」
ということです。
これで、「1個だけ渡されるのはダメ」「2個以上ならOK」といった制約を、
コンパイル時にチェックできます。
段階4:ジェネリクス+タプルで「引数リストそのもの」を型にする
「どんな引数でも受け取って、そのまま別の関数に渡す」ラッパー
可変長引数を本気で型安全に扱いたくなるのは、
「元の関数と同じ引数を受け取って、そのまま渡したい」ときです。
例えば、ログを挟むラッパー関数。
function withLog<F extends (...args: any[]) => any>(fn: F) {
return (...args: Parameters<F>): ReturnType<F> => {
console.log("call with:", args);
return fn(...args);
};
}
TypeScriptここで出てくる型がポイントです。
F extends (...args: any[]) => any
これは「可変長引数を持つ関数型」を表すジェネリックです。
Parameters<F>
これは「関数 F の引数リスト(タプル型)」を取り出すユーティリティ型です。
ReturnType<F>
これは「関数 F の戻り値の型」です。
(...args: Parameters<F>) は、
「fn と同じ引数リストを、そのまま rest引数として受け取る」
という意味になります。
例えば、fn がこういう関数だとします。
function add(a: number, b: number): number {
return a + b;
}
const loggedAdd = withLog(add);
// loggedAdd の型は (a: number, b: number) => number
TypeScriptloggedAdd は、
引数の個数・型・順番が add と完全に一致します。
ここでの超重要ポイントは、
「可変長引数を“any[]”で受けるのではなく、
“元の関数の引数リスト(タプル型)”として受けることで、
引数の個数・型・順番まで含めて型安全にできる」
ということです。
段階5:イベントハンドラやコールバックを型安全にラップする
「どんなイベントでも、その引数を崩さずに扱いたい」
もう少し現実的な例として、イベントハンドラをラップする関数を考えます。
type Handler<T extends any[]> = (...args: T) => void;
function withPrefix<T extends any[]>(handler: Handler<T>, prefix: string): Handler<T> {
return (...args: T) => {
console.log(prefix, ...args);
handler(...args);
};
}
TypeScriptここでの T extends any[] は「タプル型(配列型)」を表します。
Handler<T> は「引数リストが T の関数」です。
(...args: T) は、「T というタプルをそのまま rest引数として受け取る」という意味です。
使ってみます。
const onClick = (x: number, y: number) => {
console.log("clicked at", x, y);
};
const loggedOnClick = withPrefix(onClick, "[CLICK]");
loggedOnClick(10, 20);
// [CLICK] 10 20
// clicked at 10 20
TypeScriptonClick の引数 (x: number, y: number) が、T として withPrefix に伝わり、(...args: T) でそのまま受け取って、handler(...args) でそのまま渡しています。
ここでも、
「可変長引数を any[] ではなく、“元の関数の引数タプル”として扱う」
ことで、
「引数の個数・型・順番を崩さずにラップできる」
という型安全な設計になっています。
「型安全な可変長引数」を設計するときの視点
1つの問いに集約するとこうなる
可変長引数を設計するとき、
毎回自分にこう聞いてみてください。
「この ...args は、型の世界では“どんな配列(どんなタプル)”として扱いたい?」
同じ型だけなら T[]
最低何個か必須なら [T, T, ...T[]] のようなタプル
別の関数と同じ引数リストなら Parameters<F> や T extends any[]
というふうに、
「配列/タプルとしての形」を意識して型を付ける のが、
可変長引数を型安全にするコツです。
逆に言うと、
(...args: any[]) と書いた瞬間に、
「引数の個数・型・順番に関する情報を全部捨てている」
と思ってください。
それが本当に「何でもあり」の関数ならいいですが、
「元の関数と同じ引数を受けたい」「最低2つは必要」などの前提があるなら、
それを型として表現しておく方が、未来の自分が圧倒的に楽になります。
まとめ:可変長引数の型安全化を自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
可変長引数は、
「見た目はバラバラの引数だけど、型の世界では“1つの配列(タプル)”」
だから、
T[] で「同じ型をいくつでも」
タプル+...T[] で「最低何個か必須+残りは任意」Parameters<F> や T extends any[] で「別の関数と同じ引数リスト」
を表現できる。
コードを書くとき、
「この関数の ...args は、本当はどんな“形の引数リスト”であってほしいんだっけ?」
と一度立ち止まってから型を書くようにしてみてください。
その一呼吸で、
可変長引数は「とりあえず any[] で受けるもの」から、
あなたの設計意図をそのまま表現できる、強力な型の道具 に変わっていきます。
