【実務・中級編】引数に渡す関数の「引数の数(Arity)」を制限する型定義のテクニック – TypeScript コア・型システムの基礎解析バイブル

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 any, N extends number> =
Parameters[‘length’] extends N ? F : never;

// 使用例
const createPlugin = void>(
fn: Parameters[‘length’] extends 2 ? F : “Error: Callback must take exactly 2 arguments”
) => {
// 実装
};

// OK: 引数がちょうど2つ
createPlugin((name, age) => {});

// NG: 引数が1つしかない(コンパイルエラー:string型に割り当てられない旨のメッセージが出る)
// @ts-expect-error
createPlugin((name) => {});

—

2. 実務レベルの解決策:`StrictArity` パターンの確立

上記の `never` を返す手法は単純だが、エラーメッセージが不親切になりがちだ。実務では、「期待される引数の数」と「実際に渡された数」を型引数で比較し、不一致ならカスタムエラーメッセージを表示させるのがチーフアーキテクトの流儀だ。

以下のコードは、高階関数やReactコンポーネントのProps設計でそのまま使える「完全版」だ。

/

  • Arity(引数の数)を厳格に制限するテクニック

/

// 1. 引数の数を取得するユーティリティ
type ActualLength any> = Parameters[‘length’];

// 2. エラーメッセージを型で表現(開発者に意図を伝える)
type ArityError =
`Error: Expected exactly ${Expected} arguments, but received ${Actual}.`;

/

  • 厳格な引数チェックを行う関数定義

/
function registerStrictCallback< F extends (...args: any[]) => any
>(
fn: ActualLength extends 2
? 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[‘length’]` は、オプショナル引数を含む場合、数値リテラルではなく `number` 型や共用体型を返すことがある。

これを制御するには、Variadic Tuple Typesを用いたパターンマッチングが必要だ。

/

  • 「少なくとも1つ、最大2つ」といった範囲の制限

/
type ValidateArityRange any> =
Parameters extends [any] | [any, any]
? F
: “Error: Function must have 1 or 2 arguments”;

const handleData = any>(fn: ValidateArityRange) => {
/ … /
};

// OK
handleData((data) => {});
handleData((data, meta) => {});

// NG
// @ts-expect-error
handleData(() => {}); // 0個はダメ

—

パフォーマンスと保守性への視点

「ここまで厳密に型を書く必要があるのか?」という疑問を持つかもしれない。答えは Yes だ。

1. コンパイル時のドキュメント化:
`ArityError` のような型を定義しておくことで、後任のエンジニアが「なぜか関数が渡せない」と悩む時間をゼロにする。エラーメッセージそのものがマニュアルになる。
2. 実行時コストはゼロ:
これらはすべてコンパイル時の評価だ。JavaScriptに変換された後は、これらの複雑な型チェックは完全に消滅する。実行時のオーバーヘッドなしに、開発時の堅牢性だけを極限まで高めることができる。
3. カプセル化の促進:
不要な引数(例えば、内部でしか使わない `index` や `rawResponse`)をコールバックに露出させない設計を強制することで、APIの内部構造が漏洩するのを防げる。

結論

TypeScriptを単なる「型のアノテーションツール」として使っているうちは、まだ言語の真価を引き出せていない。

今回紹介した Arity Restriction(引数長制限) は、型システムを使って「実装の自由度を意図的に奪い、正しい道だけを歩ませる」ためのアーキテクトの武器だ。
特に、大規模なモノレポや、複数のチームが利用する共有ライブラリの設計において、このテクニックは「壊れないコード」を実現するための決定打となる。

次にコードを書くときは、その関数が「何を受け取れるか」だけでなく、「何を制限すべきか」を型で表現してみてほしい。それが、掌握への第一歩だ。

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