まず「コールバック関数」をちゃんとイメージする
コールバック関数は、ざっくり言うと
「関数に“あとで呼んでね”と渡される関数」
です。
function doTwice(callback: () => void) {
callback();
callback();
}
doTwice(() => {
console.log("呼ばれた");
});
TypeScriptここで doTwice は「関数を受け取る関数」です。
この「受け取る側」で、コールバックの型をどう書くか が今回のテーマです。
型をちゃんと書けるようになると、
「このコールバックは何を受け取って、何を返すべきか」
「呼び出し側は何を実装すればいいのか」
が一目で分かるようになります。
基本形:引数なし・戻り値なしのコールバック
一番シンプルなコールバックの型
まずは「何も受け取らず、何も返さない」コールバック。
function doTwice(callback: () => void) {
callback();
callback();
}
TypeScriptcallback: () => 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 の戻り値の型が TmapNumber の戻り値は T[]
という関係になっています。
つまり、
「コールバックの戻り値の型が、そのまま“外に返す配列の要素型”になる」
という設計です。
ここが、コールバックの型指定で一番おいしいところです。
「コールバックの型」と「関数全体の型」が、きれいにつながります。
コールバック型を「型エイリアス」に切り出す
同じ形のコールバックを何度も使うなら、名前をつける
例えば、「文字列を受け取って何かする」コールバックを、
いろんな関数で使いたいとします。
type StringHandler = (value: string) => void;
function onMessage(handler: StringHandler) {
handler("hello");
}
function onError(handler: StringHandler) {
handler("error");
}
TypeScripttype 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ここで起きていることは、
コールバックの戻り値の型が TfetchAndTransform の戻り値も 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 で名前をつけると、コードが一気に読みやすくなる。
コードを書くとき、
「このコールバックは、何を受け取って、何を約束して返す関数なんだっけ?」
と一度立ち止まってから型を書くようにしてみてください。
その一呼吸で、
コールバックは「ただの無名関数」から、
あなたの設計意図をそのまま表現する“型付きの契約” に変わっていきます。
