TypeScriptを掌握せよ:関数の「Arity(引数の数)」を型レベルで支配する極限のテクニック
フロントエンド開発において、コンポーネントのPropsやAPIのコールバック設計は、システムの堅牢性を左右する「契約」そのものだ。
しかし、多くのエンジニアがTypeScriptの「関数の互換性(Function Compatibility)」という仕様の罠に嵌まっている。TypeScriptはデフォルトで、引数が少ない関数を、引数が多い関数型として扱うことを許容する。これはJavaScriptの柔軟性を維持するための仕様だが、厳格なAPI設計においては、これが「意図しない実装」を許す脆弱性となる。
今回は、引数に渡す関数の「引数の数(Arity)」を厳密に制限し、設計者の意図から外れた実装をコンパイル時に完全に遮断する、プロフェッショナルな型定義の手法を伝授する。
—
なぜTypeScriptは「引数の数」に寛容なのか
まず、君たちが直面している現象を整理しよう。
type Callback = (id: number, metadata: string) => void;
const execute = (cb: Callback) => cb(1, “data”);
// 本来は (id, metadata) を期待しているが、(id) だけの関数も渡せてしまう
execute((id) => {
console.log(id);
});
これはバグではない。TypeScriptの仕様だ。`forEach`を思い浮かべてほしい。`(value, index, array) => void` というシグネチャに対し、我々は普段 `(item) => …` と引数を省略して書く。これが許容されないと、JavaScriptの書き心地は最悪になる。
しかし、「第2引数を無視してはいけない重要なロジック」や「パフォーマンス上の理由で余計な引数を取るクロージャを制限したい」場合、この寛容さはリスクに変わる。
—
1. Arityを固定する:基礎的な「引数長」の検証
関数の引数は、型レベルでは「タプル」として扱える。このタプルの `length` プロパティを検証することで、引数の数を制御する。
まずは、関数の引数の数が「最大N個」であることを強制するユーティリティを構築しよう。
/
- 関数の引数の数を厳密にチェックするための型ユーティリティ
/
type AssertMaxArity
Parameters
// 使用例
const createPlugin =
fn: Parameters
) => {
// 実装
};
// OK: 引数がちょうど2つ
createPlugin((name, age) => {});
// NG: 引数が1つしかない(コンパイルエラー:string型に割り当てられない旨のメッセージが出る)
// @ts-expect-error
createPlugin((name) => {});
—
2. 実務レベルの解決策:`StrictArity` パターンの確立
上記の `never` を返す手法は単純だが、エラーメッセージが不親切になりがちだ。実務では、「期待される引数の数」と「実際に渡された数」を型引数で比較し、不一致ならカスタムエラーメッセージを表示させるのがチーフアーキテクトの流儀だ。
以下のコードは、高階関数やReactコンポーネントのProps設計でそのまま使える「完全版」だ。
/
- Arity(引数の数)を厳格に制限するテクニック
/
// 1. 引数の数を取得するユーティリティ
type ActualLength
// 2. エラーメッセージを型で表現(開発者に意図を伝える)
type ArityError
`Error: Expected exactly ${Expected} arguments, but received ${Actual}.`;
/
- 厳格な引数チェックを行う関数定義
/
function registerStrictCallback<
F extends (...args: any[]) => any
>(
fn: ActualLength
? F
: ArityError<2, ActualLength
) {
// ロジック実行
}
// — テストケース —
// ✅ Pass: 引数が2つ
registerStrictCallback((a: string, b: number) => {
console.log(a, b);
});
// ❌ Fail: 引数が1つ(エラーメッセージが型として表示される)
// @ts-expect-error
registerStrictCallback((a: string) => {
console.log(a);
});
// ❌ Fail: 引数が3つ(予期せぬ引数の受け取りを拒否)
// @ts-expect-error
registerStrictCallback((a: string, b: number, c: boolean) => {
console.log(c);
});
なぜこれが重要なのか?
特に、「副作用を伴う計算」や「メモ化されたコールバック」を扱う際、不要な引数を受け取っているということは、その関数が「自分が知らなくていいコンテキスト」に依存している可能性を示唆する。これを型レベルで縛ることは、関数の責務を最小限に抑える(Single Responsibility Principle)強力な強制力となる。
—
3. オプショナル引数と「可変長引数」の制御
実務では、オプショナル引数(`arg?: type`)が混ざるケースがある。TypeScriptの `Parameters
これを制御するには、Variadic Tuple Typesを用いたパターンマッチングが必要だ。
/
- 「少なくとも1つ、最大2つ」といった範囲の制限
/
type ValidateArityRange
Parameters
? F
: “Error: Function must have 1 or 2 arguments”;
const handleData =
/ … /
};
// OK
handleData((data) => {});
handleData((data, meta) => {});
// NG
// @ts-expect-error
handleData(() => {}); // 0個はダメ
—
パフォーマンスと保守性への視点
「ここまで厳密に型を書く必要があるのか?」という疑問を持つかもしれない。答えは Yes だ。
1. コンパイル時のドキュメント化:
`ArityError` のような型を定義しておくことで、後任のエンジニアが「なぜか関数が渡せない」と悩む時間をゼロにする。エラーメッセージそのものがマニュアルになる。
2. 実行時コストはゼロ:
これらはすべてコンパイル時の評価だ。JavaScriptに変換された後は、これらの複雑な型チェックは完全に消滅する。実行時のオーバーヘッドなしに、開発時の堅牢性だけを極限まで高めることができる。
3. カプセル化の促進:
不要な引数(例えば、内部でしか使わない `index` や `rawResponse`)をコールバックに露出させない設計を強制することで、APIの内部構造が漏洩するのを防げる。
結論
TypeScriptを単なる「型のアノテーションツール」として使っているうちは、まだ言語の真価を引き出せていない。
今回紹介した Arity Restriction(引数長制限) は、型システムを使って「実装の自由度を意図的に奪い、正しい道だけを歩ませる」ためのアーキテクトの武器だ。
特に、大規模なモノレポや、複数のチームが利用する共有ライブラリの設計において、このテクニックは「壊れないコード」を実現するための決定打となる。
次にコードを書くときは、その関数が「何を受け取れるか」だけでなく、「何を制限すべきか」を型で表現してみてほしい。それが、掌握への第一歩だ。