可変長引数(rest)は「いくつ来るか分からない引数」を受け取る仕組み
まずイメージから。
可変長引数(rest 引数)は、「この関数、引数が 1 個かもしれないし、10 個かもしれない。数が決まっていないけど、全部まとめて受け取りたい」というときに使う機能です。
書き方は ...(スプレッドと同じ記号)を引数名の前につけます。
function sum(...numbers: number[]) {
let total = 0;
for (const n of numbers) {
total += n;
}
return total;
}
sum(1); // 1
sum(1, 2); // 3
sum(1, 2, 3, 4); // 10
TypeScript...numbers: number[] という書き方で、
「number を 0 個以上、いくつでも受け取る。その全部を numbers という配列として扱う」
という意味になります。
ここで大事なのは、「呼び出し側は普通の引数として並べて渡すけれど、関数の中では“配列”として扱える」という点です。
可変長引数の基本ルールと型の意味
...param: T[] は「T 型の配列として受け取る」という宣言
先ほどの例をもう少し分解します。
function sum(...numbers: number[]) {
// numbers の型は number[]
}
TypeScriptnumbers: number[] という型がついているので、関数の中では numbers は普通の number[] として扱えます。
function sum(...numbers: number[]) {
console.log(numbers); // [1, 2, 3] みたいな配列になる
return numbers.reduce((acc, n) => acc + n, 0);
}
TypeScript呼び出し側から見たときは、
sum(1, 2, 3); // 第1引数 1, 第2引数 2, 第3引数 3
TypeScriptですが、関数の中では
numbers === [1, 2, 3]
TypeScriptになっているイメージです。
TypeScript 的には、「numbers は number 型の要素を持つ配列」ということが保証されるので、numbers[0].toFixed(2) のように、number として安全に扱えます。
可変長引数と通常引数の組み合わせ
前に固定の引数、最後に可変長引数
よくある形が、「先頭に必須の引数があり、その後ろにいくつでも取れる引数が続く」パターンです。
function logWithLevel(level: "info" | "warn" | "error", ...messages: string[]) {
for (const m of messages) {
console.log(`[${level}] ${m}`);
}
}
logWithLevel("info", "start"); // 1つ
logWithLevel("warn", "something", "is", "odd"); // 複数
TypeScriptここでは、
最初の引数 level は必須
2 個目以降の引数は全部 messages に配列として入る
という構造になっています。
つまり、
logWithLevel("info", "a", "b", "c");
TypeScriptと呼び出したとき、関数内では
level === "info"
messages === ["a", "b", "c"]
TypeScriptとなります。
ルールとして、可変長引数(rest 引数)は必ず「最後の引数」に置く必要があります。
途中に置くことはできません。
// NGな例(最後じゃない)
function bad(...nums: number[], last: number) {}
// エラーになる
TypeScript可変長引数と型安全な処理の仕方
中身の型は「配列要素の型」として扱う
可変長引数を使うときは、「配列として扱う」ことを常に意識します。
function concatStrings(...parts: string[]): string {
return parts.join("-");
}
concatStrings("a"); // "a"
concatStrings("a", "b", "c"); // "a-b-c"
TypeScriptここで TypeScript は、
parts は string[]
各要素は確実に string
だと知っているので、join などの配列メソッドが型安全に使えます。
もし間違った型を渡そうとすると、呼び出し側で止まります。
// concatStrings(1, 2, 3); // エラー: number 型は string 型に渡せない
TypeScript「可変長」なのは“数”であって、“型”ではない
ここを押さえておくと、「何でも入る」わけではないことが自然に理解できます。
配列をそのまま渡すときのスプレッド構文との関係
「もう配列で持っているもの」を可変長引数に渡す
可変長引数の関数に、「すでに配列として持っている値」を渡したいことがあります。
function sum(...numbers: number[]) {
return numbers.reduce((acc, n) => acc + n, 0);
}
const nums = [1, 2, 3] as const;
// sum(nums); // これはダメ(引数1個として配列が渡されてしまう)
sum(...nums); // こう書く
TypeScriptsum(nums) と書くと、「引数が 1 個で、その中身が配列」ということになってしまいます。sum(...nums) と書くことで、
「配列 nums の中身を、“1 個ずつの引数”として展開して渡す」ことができます。
ここでの ... は、呼び出し側では「スプレッド構文」、
関数定義側では「rest 引数」と名前は違いますが、
「配列をバラす」「引数をまとめる」という対の動きをしていると思ってください。
可変長引数を設計するときに考えてほしいこと
「固定の引数」と「いくつ来てもいい引数」を分けて考える
関数を設計するとき、まずこう自分に問いかけてみてください。
この関数は、絶対に必要な情報は何か?
その情報を、どの引数で受け取るのか?
それ以外に、「何個あってもいい類の情報」はあるか?
例えばログ関数なら、
ログのレベル(info / warn / error)は必須
ログメッセージは 1 個かもしれないし 3 個かもしれない
なので、こういう型になるのが自然です。
function log(level: "info" | "warn" | "error", ...messages: string[]) {
// ...
}
TypeScript「必須なものは普通の引数、数が決まっていないものは rest 引数」
この切り分けを意識して設計すると、関数の意図が伝わりやすくなります。
まずは「数だけが可変で、型は一緒」というパターンから
可変長引数(rest)は、ジェネリクスなどと組み合わさるとかなり高度な使い方もできますが、
最初のうちは、次のようなパターンに絞って使うのがおすすめです。
同じ型の値を、いくつでも受け取れる関数を作りたいとき
前に必須の引数があり、その後ろに「同じ型の引数が何個でも続いてよい」というとき
具体的には、
sum(...numbers: number[])concatStrings(...parts: string[])log(level: "info" | "warn", ...messages: string[])
このあたりを自分の手で何回か書いてみると、
「rest 引数 = “数が決まっていない引数を配列としてまとめて受け取る”道具」という感覚が身体に入ってきます。
その感覚ができてから、
「タプル型の rest」「複数の型が混ざる rest」
といった少し高度な世界に進めば十分です。
