TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – コールバック関数の型指定

TypeScript
スポンサーリンク

まず「コールバック関数」をちゃんとイメージする

コールバック関数は、ざっくり言うと

「関数に“あとで呼んでね”と渡される関数」

です。

function doTwice(callback: () => void) {
  callback();
  callback();
}

doTwice(() => {
  console.log("呼ばれた");
});
TypeScript

ここで doTwice は「関数を受け取る関数」です。
この「受け取る側」で、コールバックの型をどう書くか が今回のテーマです。

型をちゃんと書けるようになると、

「このコールバックは何を受け取って、何を返すべきか」
「呼び出し側は何を実装すればいいのか」

が一目で分かるようになります。


基本形:引数なし・戻り値なしのコールバック

一番シンプルなコールバックの型

まずは「何も受け取らず、何も返さない」コールバック。

function doTwice(callback: () => void) {
  callback();
  callback();
}
TypeScript

callback: () => void がコールバックの型です。

() => void は、

「引数なしで呼べて、戻り値は使わない(void)関数」

という意味です。

呼び出し側はこう書きます。

doTwice(() => {
  console.log("こんにちは");
});
TypeScript

ここでのポイントは、

コールバックの型は「普通の関数型」と同じ書き方
(引数の型) => 戻り値の型 で表現する

ということです。

まずはこの形を、体に染み込ませてしまってください。


引数ありのコールバック:配列メソッドを真似してみる

「値を1つ受け取る」コールバック

例えば、「配列の各要素に対して処理をする」関数を自作するとします。

function forEachNumber(
  numbers: number[],
  callback: (value: number) => void
) {
  for (const n of numbers) {
    callback(n);
  }
}
TypeScript

ここでのコールバックの型は (value: number) => void です。

number を1つ受け取って、何も返さない関数」

という意味になります。

呼び出し側はこうです。

forEachNumber([1, 2, 3], (n) => {
  console.log(n * 2);
});
TypeScript

ここで大事なのは、

「コールバックの引数の型は、“呼び出す側が何を渡すか”で決まる」

ということです。

forEachNumber の中で callback(n) と呼んでいるので、
n の型(ここでは number)が、そのままコールバックの引数型になります。


戻り値ありのコールバック:map を自作してみる

「受け取って、変換して返す」コールバック

今度は、配列を変換する関数を考えます。

function mapNumber<T>(
  numbers: number[],
  callback: (value: number) => T
): T[] {
  const result: T[] = [];
  for (const n of numbers) {
    result.push(callback(n));
  }
  return result;
}
TypeScript

ここでのコールバックの型は (value: number) => T です。

number を受け取って、何か(型 T)を返す関数」

という意味です。

呼び出し側はこう書けます。

const lengths = mapNumber([10, 20, 30], (n) => n.toString().length);
// lengths: number[]
TypeScript

ここで起きていることを整理すると、

callback の戻り値の型が T
mapNumber の戻り値は T[]

という関係になっています。

つまり、

「コールバックの戻り値の型が、そのまま“外に返す配列の要素型”になる」

という設計です。

ここが、コールバックの型指定で一番おいしいところです。
「コールバックの型」と「関数全体の型」が、きれいにつながります。


コールバック型を「型エイリアス」に切り出す

同じ形のコールバックを何度も使うなら、名前をつける

例えば、「文字列を受け取って何かする」コールバックを、
いろんな関数で使いたいとします。

type StringHandler = (value: string) => void;

function onMessage(handler: StringHandler) {
  handler("hello");
}

function onError(handler: StringHandler) {
  handler("error");
}
TypeScript

type StringHandler = (value: string) => void;
これが「コールバックの型」に名前をつけたものです。

こうしておくと、

同じ形のコールバックを再利用できる
関数のシグネチャが読みやすくなる
「この関数は StringHandler を受け取るんだな」と一目で分かる

というメリットがあります。

呼び出し側はこうです。

const printUpper: StringHandler = (value) => {
  console.log(value.toUpperCase());
};

onMessage(printUpper);
onError(printUpper);
TypeScript

ここでのポイントは、

「コールバックの型は、type で名前をつけておくと設計が楽になる」

ということです。

「このプロジェクトでは、こういうコールバックをよく使うな」と感じたら、
迷わず型エイリアスにしてしまいましょう。


オブジェクトの中のコールバック:イベントハンドラ風の設計

「設定オブジェクトの中にコールバックを入れる」パターン

例えば、何かの処理に対して「成功時」「失敗時」のコールバックを渡したいとします。

type Callbacks = {
  onSuccess?: (value: string) => void;
  onError?: (error: Error) => void;
};

function runTask(callbacks: Callbacks) {
  try {
    const result = "OK";
    callbacks.onSuccess?.(result);
  } catch (e) {
    callbacks.onError?.(e as Error);
  }
}
TypeScript

ここでは、

onSuccess?: (value: string) => void;
onError?: (error: Error) => void;

という形で、
オブジェクトのプロパティとしてコールバックの型を指定しています。

呼び出し側はこう書けます。

runTask({
  onSuccess: (value) => {
    console.log("成功:", value);
  },
  onError: (error) => {
    console.error("失敗:", error.message);
  },
});
TypeScript

ここでのポイントは、

「コールバックを“オプションプロパティ”として持たせるときも、
型は普通に (引数) => 戻り値 で書けばいい」

ということです。

さらに、?.(オプショナルチェーン)を使うことで、
「コールバックが渡されているときだけ呼ぶ」という安全な書き方もできます。


ジェネリクスとコールバック:柔軟な関数を作る

「コールバックの戻り値が、そのまま関数の戻り値になる」パターン

例えば、「何かを取得して、それをコールバックで変換して返す」関数。

function fetchAndTransform<T>(transform: (raw: string) => T): T {
  const raw = "123"; // 本当はAPIレスポンスなど
  return transform(raw);
}
TypeScript

ここでのコールバックの型は (raw: string) => T です。

呼び出し側は、好きな型 T を「コールバックの戻り値」として決められます。

const asNumber = fetchAndTransform((raw) => Number(raw));
// asNumber: number

const asObject = fetchAndTransform((raw) => ({ value: raw }));
// asObject: { value: string }
TypeScript

ここで起きていることは、

コールバックの戻り値の型が T
fetchAndTransform の戻り値も T

という関係です。

つまり、

「コールバックの型をジェネリクスで書くことで、
呼び出し側が“結果の型”を自由に決められる関数になる」

ということです。

コールバックの型指定は、
ジェネリクスと組み合わせると一気に表現力が上がります。


「コールバック関数の型指定」で意識してほしいこと

1. 「誰が誰に何を渡すか」をはっきりさせる

コールバックの型は、
「呼び出す側が何を渡すか」「呼び出される側が何を受け取るか」の契約です。

例えば、

function forEachNumber(
  numbers: number[],
  callback: (value: number, index: number) => void
) {
  numbers.forEach((n, i) => callback(n, i));
}
TypeScript

ここでは、

value: number は「配列の要素」
index: number は「配列のインデックス」

という意味を持っています。

「このコールバックは、何を受け取るべき関数なのか?」
を、型でちゃんと表現してあげることが大事です。

2. 「戻り値を使うのか、使わないのか」を決める

コールバックの戻り値を使わないなら、=> void で十分です。

使うなら、その型をちゃんと設計に組み込みます。

function mapNumber<T>(
  numbers: number[],
  callback: (value: number) => T
): T[] {
  // ...
}
TypeScript

「戻り値をどう扱うか」で、
関数全体の型も変わってきます。

3. 「同じ形のコールバック」が増えたら、型に名前をつける

何度も (value: string) => void と書いているなら、
type StringHandler = (value: string) => void;
と名前をつけた方が、読みやすく・変更しやすくなります。

「このプロジェクトにとって大事な“コールバックの形”」は、
型としてちゃんと名前を持たせてあげると、設計が一段上がります。


まとめ:コールバック関数の型指定を自分の言葉で言うと

最後に、あなた自身の言葉でこう整理してみてください。

コールバックの型は、

「この関数に“あとで呼んでね”と渡す関数が、
どんな引数を受け取って、何を返すべきかの契約」

であり、

(引数の型) => 戻り値の型 という普通の関数型で表現できる。
引数の型は「呼び出す側が何を渡すか」から決まる。
戻り値の型は「その結果をどう使うか」で関数全体の設計とつながる。
よく使う形は type で名前をつけると、コードが一気に読みやすくなる。

コードを書くとき、
「このコールバックは、何を受け取って、何を約束して返す関数なんだっけ?」
と一度立ち止まってから型を書くようにしてみてください。

その一呼吸で、
コールバックは「ただの無名関数」から、
あなたの設計意図をそのまま表現する“型付きの契約” に変わっていきます。

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