ゴール:bind / call / apply を「型の目」で理解する
ここで目指したいのは、
「bind / call / apply が“何をするメソッドか”」だけでなく、
「TypeScript 的には、どんな型の関数に、どんな引数を渡せるのか」
をちゃんと説明できる状態です。
特に大事なのは次の3つです。
- this を持つ関数に対して、bind / call / apply がどう型チェックされるか
- this パラメータ(
this: 型)と組み合わせたときの挙動 strictBindCallApplyオプションがオンのときの“ガチガチな型チェック”
ここを押さえると、「なんとなく動く」から「型で守られた設計」に一段上がれます。
前提整理:bind / call / apply がやっていること
call / apply:this と引数を指定して「今すぐ呼ぶ」
まずは call と apply から。
function greet(this: { name: string }, message: string) {
console.log(`${message}, ${this.name}!`);
}
const user = { name: "山田" };
greet.call(user, "こんにちは"); // こんにちは, 山田!
greet.apply(user, ["こんばんは"]); // こんばんは, 山田!
TypeScriptやっていることはシンプルです。
call(thisArg, arg1, arg2, ...)
this と引数をバラで渡して、その場で呼ぶapply(thisArg, argsArray)
this と「引数の配列」を渡して、その場で呼ぶ
TypeScript 的に重要なのは、
- thisArg の型が、関数の this パラメータの型と合っているか
- 引数の型・個数が、関数の引数と合っているか
をチェックしてくれる、という点です(strictBindCallApply が true の場合)。
bind:this と一部の引数を「固定した新しい関数」を返す
bind は「今すぐ呼ぶ」のではなく、「あとで呼べる関数を作る」メソッドです。
function greet(this: { name: string }, message: string) {
console.log(`${message}, ${this.name}!`);
}
const user = { name: "山田" };
const greetUser = greet.bind(user);
greetUser("こんにちは"); // こんにちは, 山田!
TypeScriptgreet.bind(user) の戻り値は、
- this が
userに固定された - それ以外の引数(ここでは
message: string)を受け取る関数
です。
TypeScript は、この「戻り値の関数の型」までちゃんと計算してくれます(strictBindCallApply 有効時)。 TypeScript
this パラメータ付き関数と call / apply の型チェック
this パラメータを付けた関数を call で呼ぶ
まず、this パラメータ付きの関数を定義します。
function increment(this: { value: number }, amount: number) {
this.value += amount;
}
TypeScriptこの関数の型は、ざっくり言うと
(this: { value: number }, amount: number) => void
TypeScriptです。
呼び出し側:
const counter = { value: 0 };
increment.call(counter, 5);
// counter.value === 5
TypeScriptここで TypeScript は、
counterが{ value: number }を満たしているか5がamount: numberに合っているか
をチェックします。
もし this の型が合わなければ、コンパイルエラーになります。
const bad = { count: 0 };
// increment.call(bad, 5); // エラー: 'value' プロパティがない
TypeScriptここでの重要ポイントは、
「this パラメータを明示しておくと、call / apply で“間違った this”を渡した瞬間に型エラーになる」
ということです。
apply の場合:引数は配列(タプル)でチェックされる
同じ関数を apply で呼ぶとこうなります。
const counter = { value: 0 };
increment.apply(counter, [5]); // OK
increment.apply(counter, [5, 10]); // エラー(引数が多い)
increment.apply(counter, []); // エラー(引数が足りない)
TypeScriptstrictBindCallApply が true のとき、apply の第2引数は「引数のタプル型」としてチェックされます。
[5]は[number]として OK[5, 10]は「2つ目の引数が余計」なのでエラー[]は「必須の引数が足りない」のでエラー
「apply は配列だから何でも入る」ではなく、
「配列の中身まで“何個・何型”かを見られる」 というのがポイントです。
bind と this パラメータ:戻り値の関数型まで意識する
this だけを固定する bind
さっきの greet をもう一度使います。
function greet(this: { name: string }, message: string) {
console.log(`${message}, ${this.name}!`);
}
const user = { name: "山田" };
const greetUser = greet.bind(user);
TypeScriptgreetUser の型はどうなるかというと、
(message: string) => void
TypeScriptです。
- this は
userに固定されたので、もう引数としては不要 - 残りの引数
message: stringだけを受け取る関数
という形になります。
呼び出し側:
greetUser("こんにちは"); // OK
// greetUser(); // エラー: message が足りない
TypeScriptstrictBindCallApply が true のとき、
「元の関数が期待している引数」と「bind 後の関数が受け取るべき引数」が
きっちり型でつながります。
this と一部の引数をまとめて固定する bind
bind は this だけでなく、先頭の引数も一緒に固定できます。
function greet(this: { name: string }, message: string, punctuation: string) {
console.log(`${message}, ${this.name}${punctuation}`);
}
const user = { name: "山田" };
// this と message を固定
const excitedGreet = greet.bind(user, "やあ");
excitedGreet("!"); // やあ, 山田!
TypeScriptここでの型の流れはこうです。
元の関数:
(this: { name: string }, message: string, punctuation: string) => void
TypeScriptbind(user, "やあ") の戻り値:
(punctuation: string) => void
TypeScript- this →
userに固定 message→"やあ"に固定- 残りの
punctuation: stringだけが引数として残る
この「残りの引数だけを受け取る関数型」を、TypeScript がちゃんと計算してくれます。
strictBindCallApply オプションと「型の厳しさ」
strictBindCallApply が false の世界(ゆるい世界)
strictBindCallApply が false の場合(古い設定や strict 無効時)、bind / call / apply の型はかなりゆるく扱われます。
- 引数の型が多少違っても通ってしまう
- 引数の個数が足りなくても通ってしまう
- 戻り値の型が
anyになってしまうこともある
その結果、「コンパイルは通るけど実行時に落ちる」コードが簡単に書けてしまいます。
strictBindCallApply が true の世界(ちゃんと守ってくれる世界)
strict: true にしていると自動で有効になるのが strictBindCallApply です。
これが true だと、
callの引数の型・個数が厳密にチェックされるapplyの配列の中身(タプル)が厳密にチェックされるbindの thisArg と部分適用する引数が厳密にチェックされるbindの戻り値の関数型も、元の関数から正しく導かれる
ようになります。
例えば:
function foo(a: number, b: string): string {
return a + b;
}
foo.apply(undefined, [10]); // エラー: 引数が足りない
foo.apply(undefined, [10, 20]); // エラー: 2つ目が number
foo.apply(undefined, [10, "hello"]); // OK
foo.apply(undefined, [10, "hello", 1]); // エラー: 多すぎる
TypeScriptこのレベルでチェックしてくれるので、
「bind / call / apply を使うときも、普通の関数呼び出しと同じくらい型安全」
な世界になります。
ユーティリティ型と組み合わせて「自分で bind っぽい関数」を作る
Parameters / ReturnType で「元の関数の型」を引き継ぐ
bind の型の考え方は、自分で高階関数を書くときにも使えます。
例えば、「ログを出しつつ関数を呼ぶラッパー」を作るとします。
function withLog<F extends (...args: any[]) => any>(fn: F) {
return (...args: Parameters<F>): ReturnType<F> => {
console.log("args:", args);
const result = fn(...args);
console.log("result:", result);
return result;
};
}
TypeScriptここで使っているのが Parameters<F> と ReturnType<F> です。
Parameters<F>
関数 F の引数リスト(タプル型)ReturnType<F>
関数 F の戻り値の型
bind も内部的には同じような発想で、
「元の関数の this と引数の型」から「新しい関数の引数の型」を計算しています。
bind / call / apply の型を理解しておくと、
「自分で“型安全なラッパー関数”を書くときの設計のヒント」 にもなります。
まとめ:bind / call / apply と型を自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
call/applyは「this と引数を指定して今すぐ呼ぶ」
this パラメータ付き関数と組み合わせると、
「this の型」と「引数の型・個数」がきっちりチェックされる。bindは「this と一部の引数を固定した“新しい関数”を返す」
戻り値の関数型は、「元の関数から this と固定済みの引数を引いた残り」で決まる。strictBindCallApplyを true にすると、
bind / call / apply も普通の関数呼び出しと同じくらい厳密に型チェックされる。
コードを書くとき、
「ここで bind / call / apply を使うなら、this と引数の型は本当に合っているか?」
「this パラメータをちゃんと宣言して、型に守ってもらえる形にできないか?」
と一度立ち止まってみてください。
その一呼吸で、
bind / call / apply は「危なそうな低レベルメソッド」から、
“型でコントロールされた、安心して使える道具” に変わっていきます。
