TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – 可変長引数の型安全化

TypeScript
スポンサーリンク

「可変長引数」は型の世界だとどう見えるのか

まず、可変長引数は 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
TypeScript

loggedAdd は、
引数の個数・型・順番が 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
TypeScript

onClick の引数 (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[] で受けるもの」から、
あなたの設計意図をそのまま表現できる、強力な型の道具 に変わっていきます。

タイトルとURLをコピーしました