「関数を引数に取る」とは、責任を“相手に渡す”設計
まずイメージからいきます。
function doTwice(fn: () => void) {
fn();
fn();
}
doTwice(() => {
console.log("呼ばれた");
});
TypeScriptdoTwice は「何を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ここでの設計判断は、
「関数を引数に取るのは強力だけど、
数が増えたら“オブジェクトにまとめる”方が読みやすい」
ということです。
「関数を引数に取る設計」は強いけれど、
“増えすぎたら構造化する”というバランス感覚が大事 です。
まとめ:「関数を引数に取る設計」を自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
関数を引数に取る設計は、
「自分の関数は“いつ・どう呼ぶか”という枠だけを持ち、
“何をするか”は引数として渡された関数に任せる」
という責任分担のための仕組み。
設計するときは、
この関数は、どんな“枠”を提供したいのか?
どのタイミングで、どんな情報をコールバックに渡したいのか?
関数引数が増えすぎていないか?(増えたらオブジェクトにまとめる)
ジェネリクスで「どんな関数でも包める枠」にしたい場面か?
を一度立ち止まって考える。
その一呼吸を挟めるようになると、
「関数を引数に取る」はただのテクニックではなく、
“流れと中身をきれいに分離するための設計の武器” に変わっていきます。
