TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – bind / call / apply と型

TypeScript
スポンサーリンク

ゴール:bind / call / apply を「型の目」で理解する

ここで目指したいのは、

「bind / call / apply が“何をするメソッドか”」だけでなく、
「TypeScript 的には、どんな型の関数に、どんな引数を渡せるのか」
をちゃんと説明できる状態です。

特に大事なのは次の3つです。

  • this を持つ関数に対して、bind / call / apply がどう型チェックされるか
  • this パラメータ(this: 型)と組み合わせたときの挙動
  • strictBindCallApply オプションがオンのときの“ガチガチな型チェック”

ここを押さえると、「なんとなく動く」から「型で守られた設計」に一段上がれます。


前提整理:bind / call / apply がやっていること

call / apply:this と引数を指定して「今すぐ呼ぶ」

まずは callapply から。

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("こんにちは"); // こんにちは, 山田!
TypeScript

greet.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 } を満たしているか
  • 5amount: 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, []);      // エラー(引数が足りない)
TypeScript

strictBindCallApply が 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);
TypeScript

greetUser の型はどうなるかというと、

(message: string) => void
TypeScript

です。

  • this は user に固定されたので、もう引数としては不要
  • 残りの引数 message: string だけを受け取る関数

という形になります。

呼び出し側:

greetUser("こんにちは"); // OK
// greetUser();          // エラー: message が足りない
TypeScript

strictBindCallApply が 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
TypeScript

bind(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 は「危なそうな低レベルメソッド」から、
“型でコントロールされた、安心して使える道具” に変わっていきます。

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