「コールバック関数の型」とは何か
まず、「コールバック関数」という言葉をちゃんと掴みましょう。
コールバック関数は、「関数に“引数として渡される関数”」のことです。
function doTwice(fn: () => void) {
fn();
fn();
}
doTwice(() => {
console.log("Hello");
});
TypeScriptここでは doTwice が「呼ぶ側」、fn が「渡される側の関数(コールバック)」です。
TypeScript では、この「渡される側の関数」にもきちんと型を付けます。
それが 「コールバック関数の型」 です。
やりたいことはシンプルで、
「このコールバックは、どんな引数を受け取って、どんな値を返す関数なのか」を、型で表現したいだけです。
一番基本の形:引数なし・戻り値なしのコールバック
一番シンプルなコールバック型から見てみます。
function runCallback(fn: () => void) {
fn();
}
runCallback(() => {
console.log("実行されました");
});
TypeScriptfn: () => void という型は、「引数なし・戻り値なしの関数」という意味です。
() => void を分解すると、
かっこの中が「引数のリスト」、=> の右側が「戻り値の型」です。
つまり、
引数:なし
戻り値:void(使う値は返さない関数)
という型のコールバックだけを受け取る関数、ということになります。
例えば、次はエラーになります。
// runCallback((msg: string) => { console.log(msg); });
// エラー:() => void を期待しているのに (msg: string) => void を渡している
TypeScript「引数を取らない約束なのに、引数ありの関数」を渡そうとしているからです。
「コールバックの“形(シグネチャ)”を合っているかどうかチェックしてくれる」のが、コールバック関数の型の役目です。
引数を取るコールバックの型
例:配列の要素を1つずつ処理するコールバック
配列の forEach や map は、まさに「コールバック関数」を使っています。
const numbers = [1, 2, 3];
numbers.forEach((n) => {
console.log(n * 2);
});
TypeScriptforEach のコールバックの型は、おおざっぱに言うとこうです。
(value: number, index: number, array: number[]) => void
TypeScriptつまり、
value:配列の要素(number)
index:インデックス番号(number)
array:配列そのもの(number[])
戻り値:void
という形の関数をコールバックとして受け付けます。
自分で似たような関数を書くとしたら、こんな感じです。
function forEachNumber(
numbers: number[],
callback: (value: number, index: number) => void
) {
for (let i = 0; i < numbers.length; i++) {
callback(numbers[i], i);
}
}
forEachNumber([1, 2, 3], (value, index) => {
console.log(index, value * 2);
});
TypeScriptここでcallback: (value: number, index: number) => void
が「コールバック関数の型」です。
このおかげで、
value は必ず number として扱える(toFixed などが安心して呼べる)index も number として扱える
戻り値は使わない(void)
ということがコンパイル時点で保証されます。
コールバック型を type で名前をつけて再利用する
コールバックの型は、毎回 (arg: 型) => 〜 と書いてもいいですが、
何度も同じ形が出てくるなら、型に名前を付けるとコードが見通しやすくなります。
type NumberCallback = (value: number, index: number) => void;
function forEachNumber(numbers: number[], callback: NumberCallback) {
for (let i = 0; i < numbers.length; i++) {
callback(numbers[i], i);
}
}
TypeScriptこのようにすると、
NumberCallback 型を見るだけで、「このコールバックは number と index を受け取って void を返すんだな」と分かる
複数の関数で同じ型を再利用できる
というメリットがあります。
他にも、例えば「文字列を加工するコールバック」ならこう書けます。
type StringTransformer = (value: string) => string;
function transformAll(values: string[], fn: StringTransformer): string[] {
return values.map(fn);
}
const upper: StringTransformer = (v) => v.toUpperCase();
const withMark: StringTransformer = (v) => v + "!";
transformAll(["a", "b"], upper); // ["A", "B"]
transformAll(["a", "b"], withMark); // ["a!", "b!"]
TypeScriptここでは、「文字列を受け取って文字列を返す関数」という“役割”に名前を付けているイメージです。
コールバックの戻り値型もちゃんと考える
コールバックというと「void でしょ」と思いがちですが、
戻り値をしっかり型にした方がいい場面もたくさんあります。
例:フィルター関数
type Predicate<T> = (value: T) => boolean;
function filterNumbers(values: number[], pred: Predicate<number>): number[] {
const result: number[] = [];
for (const v of values) {
if (pred(v)) {
result.push(v);
}
}
return result;
}
const isEven: Predicate<number> = (n) => n % 2 === 0;
const evens = filterNumbers([1, 2, 3, 4], isEven); // [2, 4]
TypeScriptここでの Predicate<number> は「number を受け取って boolean を返すコールバック型」です。
この型のおかげで、
pred の戻り値を if 文の条件に安心して使える
間違えて string を返そうとするとエラーになる
という形で、「コールバックが守るべき約束」を型で縛ることができます。
コールバック型を見るときに意識してほしいポイント
コールバック関数の型を読む/書くとき、必ず次の3つに分解して考えてください。
このコールバックは、何個の引数を受け取る?
それぞれの引数は、何という型?
戻り値は、何という型?
例えば、
type Loader = (url: string, retryCount?: number) => Promise<string>;
TypeScriptこれは、「コールバック型の自己紹介」です。
言葉にすると、
1つ目の引数:必須の url: string
2つ目の引数:省略可能な retryCount?: number
戻り値:Promise<string>(非同期に文字列を返す)
という「関数の形」です。
この型を使った関数は、こうなります。
function useLoader(loader: Loader) {
return loader("https://example.com", 3);
}
TypeScriptそして渡す側のコールバックは、型を守る必要があります。
const loaderImpl: Loader = async (url, retryCount = 0) => {
// url: string
// retryCount: number
// 戻り値は Promise<string> でなければならない
const res = await fetch(url);
return res.text();
};
TypeScript「コールバックの型 = “関数の形”を伝える契約書」
この感覚を持てると、コールバックの設計が一気にクリアになります。
まずはシンプルなコールバック型から慣れていく
いきなり複雑なジェネリクス付きのコールバック型を理解しようとしなくて大丈夫です。
最初は、次のような素朴なパターンを自分の手で書いてみてください。
引数なし・戻り値なし:() => void
1引数・戻り値なし:(value: T) => void
1引数・戻り値あり:(value: T) => U
配列の要素を処理:(value: T, index: number) => void
そして必ず、
「このコールバックは、何を受け取って、何を返す役割なのか?」
「それを type で名前をつけて表現できるか?」
という視点で設計してみてください。
コールバック関数の型は、
「ただの文法」ではなく、「関数同士の約束ごとをコードで表現するための言葉」です。
その“言葉”を少しずつ使えるようになると、TypeScript が本当に面白くなってきます。
