TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – 関数を引数に取る設計

TypeScript
スポンサーリンク

「関数を引数に取る」とは、責任を“相手に渡す”設計

まずイメージからいきます。

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

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

doTwice は「何を2回やるか」を知りません。
「2回やる」という“枠”だけを持っていて、
「中身」は呼び出し側が関数として渡します。

これが「関数を引数に取る設計」の本質です。

自分の関数は「いつ・どのタイミングで呼ぶか」だけを決める。
「具体的に何をするか」は、引数として渡された関数に任せる。

つまり、
「処理の流れ」と「具体的な中身」を分離するための道具
として「関数を引数に取る」という設計があります。


基本形:コールバックを1つ受け取る関数設計

「終わったら呼んでね」というコールバック

よくあるパターンが「処理が終わったら呼ぶ」コールバックです。

function runTask(callback: () => void) {
  console.log("タスク開始");
  // 何か重い処理…
  console.log("タスク終了");
  callback();
}

runTask(() => {
  console.log("完了後の処理");
});
TypeScript

ここでの設計を言葉にすると、

runTask の責任:
「タスクを実行して、終わったタイミングで callback を呼ぶ」

callback の責任:
「終わったあとに何をするかを決める」

です。

型としては、callback: () => void
「引数なしで呼べて、戻り値は使わない関数」を受け取る、という契約です。

ここで大事なのは、

「関数を引数に取る=“このタイミングで、あなたの処理を呼びますよ”という約束を作ること」

だと理解することです。

「値を渡して呼ぶ」コールバック

次に、「結果を渡して呼ぶ」パターン。

function fetchUser(callback: (name: string) => void) {
  const name = "Taro"; // 本当はAPIなど
  callback(name);
}

fetchUser((name) => {
  console.log("ユーザー名:", name);
});
TypeScript

ここでは、

fetchUser の責任:
「ユーザー名を取得して、callback に渡す」

callback の責任:
「渡されたユーザー名をどう扱うか決める」

型としては、callback: (name: string) => void
「string を1つ受け取って、何かする関数」です。

ポイントは、「関数の中で callback に何を渡すか」が、そのまま callback の引数型になる ことです。
設計するときは、まず「このタイミングで、何を渡して呼びたいか」を決めます。


設計の視点1:「どこまでを“自分の関数の責任”にするか」

「枠」と「中身」を分けて考える

例えば、ログ付きで処理を実行する関数を考えます。

function withLog(taskName: string, fn: () => void) {
  console.log(`[START] ${taskName}`);
  fn();
  console.log(`[END] ${taskName}`);
}

withLog("データ更新", () => {
  console.log("DB を更新中…");
});
TypeScript

ここでの設計はこうです。

withLog の責任:
「前後にログを出しつつ、渡された処理を実行する」

fn の責任:
「実際に何をするか(DB 更新など)を決める」

「関数を引数に取る設計」をするとき、
まず考えるべきはこの問いです。

自分の関数は、“どんな枠”だけを提供したいのか?

前後にログを出す枠
エラーをキャッチする枠
リトライする枠
トランザクションを張る枠

など、「共通の流れ」を自分の関数が持ち、
「中身」は引数の関数に任せる。

この分離がうまくいくと、
「流れの再利用」と「中身の自由度」が両立します。


設計の視点2:関数引数の「型」をどう決めるか

「何を渡して呼ぶか」から逆算する

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

function forEachNumber(
  numbers: number[],
  fn: (value: number, index: number) => void
) {
  for (let i = 0; i < numbers.length; i++) {
    fn(numbers[i], i);
  }
}
TypeScript

ここでの設計は、

fn に渡すもの:
value(要素の number)と index(インデックス)

だから、型は (value: number, index: number) => void になる。

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

forEachNumber([10, 20, 30], (value, index) => {
  console.log(index, value * 2);
});
TypeScript

関数を引数に取る設計では、
「このタイミングで、何を渡して呼びたいか」 を先に決めると、
自然に関数引数の型が決まります。

逆に言うと、

「何を渡すか決めていないのに、とりあえず () => void にしておく」

のは、設計として弱いです。

「この関数は、どんな情報をコールバックに渡すべきか?」
を一度ちゃんと考えることが、型設計の第一歩になります。


設計の視点3:ジェネリクスで「どんな関数でも受けられる枠」を作る

「どんな処理でもラップできる」関数を作る

例えば、「処理の実行時間を測る」関数を作りたいとします。

function measure<F extends (...args: any[]) => any>(fn: F): F {
  return ((...args: any[]) => {
    const start = performance.now();
    const result = fn(...args);
    const end = performance.now();
    console.log(`time: ${end - start}ms`);
    return result;
  }) as F;
}
TypeScript

少し難しそうに見えますが、やっていることはシンプルです。

measure の責任:
「どんな関数でも受け取って、実行時間を測る“ラッパー関数”を返す」

fn の責任:
「実際の処理(引数・戻り値は自由)」

ここでの型のポイントは、

F extends (...args: any[]) => any
「引数いくつか・何かを返す関数型」

fn: F
「その具体的な関数」

measure の戻り値:F
「元の関数と同じ型の関数を返す」

という設計です。

使ってみると、こうなります。

function add(a: number, b: number): number {
  return a + b;
}

const measuredAdd = measure(add);

const result = measuredAdd(3, 5);
// time: 〇〇ms
// result: 8
TypeScript

ここでの重要ポイントは、

「関数を引数に取る設計」と「ジェネリクス」を組み合わせると、
“どんな関数でも包み込める枠”を型安全に作れる

ということです。

「関数を引数に取る」設計を一段深くやるとき、
ジェネリクスはほぼ必須の相棒になります。


設計の視点4:「関数を引数に取る」か「クラスやオブジェクトを渡す」か

どこまでを「1つの関数」に押し込めるか

例えば、「タスクのライフサイクル」を扱いたいとします。

開始時に何かしたい
成功時に何かしたい
失敗時に何かしたい

これを全部「関数引数」でやろうとすると、こうなります。

function runTask(
  onStart: () => void,
  onSuccess: (result: string) => void,
  onError: (error: Error) => void
) {
  onStart();
  try {
    const result = "OK";
    onSuccess(result);
  } catch (e) {
    onError(e as Error);
  }
}
TypeScript

引数が増えてきて、読みづらくなります。

こういうときは、「オブジェクトにまとめる」設計も候補になります。

type TaskCallbacks = {
  onStart?: () => void;
  onSuccess?: (result: string) => void;
  onError?: (error: Error) => void;
};

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

ここでの設計判断は、

「関数を引数に取るのは強力だけど、
数が増えたら“オブジェクトにまとめる”方が読みやすい」

ということです。

「関数を引数に取る設計」は強いけれど、
“増えすぎたら構造化する”というバランス感覚が大事
です。


まとめ:「関数を引数に取る設計」を自分の言葉で言うと

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

関数を引数に取る設計は、

「自分の関数は“いつ・どう呼ぶか”という枠だけを持ち、
“何をするか”は引数として渡された関数に任せる」

という責任分担のための仕組み。

設計するときは、

この関数は、どんな“枠”を提供したいのか?
どのタイミングで、どんな情報をコールバックに渡したいのか?
関数引数が増えすぎていないか?(増えたらオブジェクトにまとめる)
ジェネリクスで「どんな関数でも包める枠」にしたい場面か?

を一度立ち止まって考える。

その一呼吸を挟めるようになると、
「関数を引数に取る」はただのテクニックではなく、
“流れと中身をきれいに分離するための設計の武器” に変わっていきます。

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