TypeScript | 基礎文法:関数の基礎 – コールバック関数の型

TypeScript
スポンサーリンク

「コールバック関数の型」とは何か

まず、「コールバック関数」という言葉をちゃんと掴みましょう。
コールバック関数は、「関数に“引数として渡される関数”」のことです。

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

doTwice(() => {
  console.log("Hello");
});
TypeScript

ここでは doTwice が「呼ぶ側」、fn が「渡される側の関数(コールバック)」です。
TypeScript では、この「渡される側の関数」にもきちんと型を付けます。
それが 「コールバック関数の型」 です。

やりたいことはシンプルで、
「このコールバックは、どんな引数を受け取って、どんな値を返す関数なのか」を、型で表現したいだけです。


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

一番シンプルなコールバック型から見てみます。

function runCallback(fn: () => void) {
  fn();
}

runCallback(() => {
  console.log("実行されました");
});
TypeScript

fn: () => void という型は、「引数なし・戻り値なしの関数」という意味です。

() => void を分解すると、
かっこの中が「引数のリスト」、=> の右側が「戻り値の型」です。
つまり、

引数:なし
戻り値:void(使う値は返さない関数)

という型のコールバックだけを受け取る関数、ということになります。

例えば、次はエラーになります。

// runCallback((msg: string) => { console.log(msg); });
// エラー:() => void を期待しているのに (msg: string) => void を渡している
TypeScript

「引数を取らない約束なのに、引数ありの関数」を渡そうとしているからです。
「コールバックの“形(シグネチャ)”を合っているかどうかチェックしてくれる」のが、コールバック関数の型の役目です。


引数を取るコールバックの型

例:配列の要素を1つずつ処理するコールバック

配列の forEachmap は、まさに「コールバック関数」を使っています。

const numbers = [1, 2, 3];

numbers.forEach((n) => {
  console.log(n * 2);
});
TypeScript

forEach のコールバックの型は、おおざっぱに言うとこうです。

(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 などが安心して呼べる)
indexnumber として扱える
戻り値は使わない(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 が本当に面白くなってきます。

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