まず「高階関数」をちゃんと定義しておく
高階関数(higher-order function)は、ざっくり言うと
「関数を受け取る」か「関数を返す」か、あるいはその両方をする関数
のことです。
function doTwice(fn: () => void) {
fn();
fn();
}
function createAdder(a: number) {
return (b: number) => a + b;
}
TypeScriptdoTwice は「関数を引数に取る関数」、createAdder は「関数を返す関数」です。
TypeScript で高階関数を扱うときのポイントは、
「関数を“値”として扱う」
「その関数にも、ちゃんと型(引数・戻り値)がある」
この2つを、型レベルでしっかり表現してあげることです。
基本1:関数を「引数として受け取る」高階関数の型付け
一番シンプルな高階関数
まずは「引数として関数を受け取る」パターンから。
function doTwice(fn: () => void) {
fn();
fn();
}
TypeScriptここでの型は fn: () => void です。
「引数なしで呼べて、戻り値を使わない関数」
という意味になります。
呼び出し側はこう書きます。
doTwice(() => {
console.log("呼ばれた");
});
TypeScriptここで大事なのは、
高階関数の「関数引数」は、
普通の関数型と同じく (引数の型) => 戻り値の型 で書く
という感覚です。
まずはこのレベルを、当たり前に書けるようにしておきます。
引数ありの関数を受け取る高階関数
例えば、「配列の各要素に対して処理をする」高階関数。
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: number) => void。
「要素とインデックスを受け取って、何かする関数」
という契約です。
呼び出し側はこうなります。
forEachNumber([10, 20, 30], (value, index) => {
console.log(index, value * 2);
});
TypeScriptポイントは、
「高階関数の中で fn に何を渡しているか」が、そのまま fn の引数型になる
ということです。
基本2:関数を「返す」高階関数の型付け
関数を返す=「戻り値の型が関数型」
次は「関数を返す」パターン。
function createAdder(a: number) {
return (b: number) => a + b;
}
TypeScript型をちゃんと書くとこうです。
function createAdder(a: number): (b: number) => number {
return (b: number) => a + b;
}
TypeScriptcreateAdder の型は、
「number を受け取って、(b: number) => number を返す関数」
です。
使う側はこうなります。
const add10 = createAdder(10);
// add10: (b: number) => number
console.log(add10(5)); // 15
console.log(add10(20)); // 30
TypeScriptここでの重要ポイントは、
「高階関数の戻り値の型として、“関数型”をそのまま書く」
ということです。
(引数) => 戻り値 を、
そのまま戻り値の型として使うイメージを持ってください。
基本3:関数を「受け取って返す」高階関数(ラッパー関数)
ログを挟むラッパー関数を例にする
高階関数らしさが一気に出るのが、「関数を受け取って、関数を返す」パターンです。
function withLog(fn: () => void) {
return () => {
console.log("start");
fn();
console.log("end");
};
}
TypeScript型だけ抜き出すと、
(fn: () => void) => () => void
TypeScriptです。
「() => void を受け取って、() => void を返す関数」
という意味になります。
使ってみます。
const sayHello = () => {
console.log("こんにちは");
};
const loggedHello = withLog(sayHello);
loggedHello();
// start
// こんにちは
// end
TypeScriptここでのポイントは、
「高階関数は、“関数の振る舞いをラップして、同じ形の関数を返す”ことが多い」
ということです。
このとき、
「受け取る関数」と「返す関数」の型をどう一致させるか、が設計の肝になります。
一段上:ジェネリクスで「どんな関数でもラップできる」高階関数
any で妥協しない書き方
さっきの withLog を、どんな関数にも使えるようにしたいとします。
雑に書くとこうなります。
function withLogAny(fn: (...args: any[]) => any) {
return (...args: any[]) => {
console.log("call with:", args);
return fn(...args);
};
}
TypeScriptこれは動きますが、any だらけで型安全ではありません。
TypeScript らしく書くなら、ジェネリクス+ユーティリティ型を使います。
function withLog<F extends (...args: any[]) => any>(fn: F) {
return (...args: Parameters<F>): ReturnType<F> => {
console.log("call with:", args);
return fn(...args);
};
}
TypeScriptここで出てくる型が重要です。
F extends (...args: any[]) => any
「引数をいくつか取り、何かを返す関数型」
Parameters<F>
「関数 F の引数リスト(タプル型)」
ReturnType<F>
「関数 F の戻り値の型」
(...args: Parameters<F>): ReturnType<F> は、
「F と同じ引数を受け取り、F と同じ型を返す関数」
という意味になります。
使ってみます。
function add(a: number, b: number): number {
return a + b;
}
const loggedAdd = withLog(add);
// loggedAdd: (a: number, b: number) => number
const result = loggedAdd(3, 5);
// call with: [3, 5]
// result: 8
TypeScriptここでの超重要ポイントは、
「高階関数の型をジェネリクスで書くと、
“元の関数の引数・戻り値”をそのまま引き継いだラッパーを作れる」
ということです。
any[] ではなく Parameters<F>、any ではなく ReturnType<F> を使うことで、
引数の個数・型・順番・戻り値まで、すべて型安全に保てます。
型エイリアスで「高階関数の型」を整理する
関数型に名前をつけると、一気に読みやすくなる
例えば、「文字列を受け取って何かを返す関数」をよく扱うなら、
こういう型を用意できます。
type StringTransformer<T> = (value: string) => T;
function withTrim<T>(fn: StringTransformer<T>): StringTransformer<T> {
return (value: string) => fn(value.trim());
}
TypeScriptここでは、
StringTransformer<T> が「高階関数で扱う関数の“形”」withTrim が「その関数を受け取って、同じ形の関数を返す高階関数」
という関係になっています。
使ってみます。
const toLength: StringTransformer<number> = (value) => value.length;
const toLengthAfterTrim = withTrim(toLength);
console.log(toLengthAfterTrim(" hello ")); // 5
TypeScriptここでのポイントは、
「高階関数で扱う“関数の形”に名前をつけておくと、
高階関数のシグネチャが一気に読みやすくなる」
ということです。
(value: string) => T を何度も書くより、StringTransformer<T> としておいた方が、
「何を扱っているのか」がすぐに伝わります。
高階関数の型付けで意識してほしいこと
「関数の型」を2段階で見る癖をつける
高階関数の型は、よく見ると「二重構造」になっています。
例えば、さっきの withLog。
function withLog<F extends (...args: any[]) => any>(
fn: F
): (...args: Parameters<F>) => ReturnType<F> {
// ...
}
TypeScriptこれを分解すると、
外側の関数の型:(fn: F) => (...args: Parameters<F>) => ReturnType<F>
内側で扱っている関数の型:F(= (...args: any[]) => any の具体形)
という二重構造です。
高階関数を読むとき・書くときは、
「外側の関数は、どんな関数を受け取って、どんな関数を返すのか?」
「内側で扱っている“関数そのもの”の引数・戻り値はどうなっているか?」
この2段階で見る癖をつけると、一気に理解しやすくなります。
まとめ:高階関数の型付けを自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
高階関数は、
「関数を受け取る」「関数を返す」「その両方をする」関数で、
型の世界では「関数型を引数・戻り値として扱う関数」。
基本は、
関数を受け取るときは fn: (args) => result
関数を返すときは (): (args) => result
両方やるときは (fn: F) => (...args: Parameters<F>) => ReturnType<F>
ジェネリクスと Parameters / ReturnType を組み合わせると、
「元の関数と同じ引数・戻り値を持つラッパー」を型安全に作れる。
コードを書くとき、
「この高階関数は、“どんな関数”を受け取って、“どんな関数”を返すんだっけ?」
と一度立ち止まって、外側と内側の関数の型を分けて考えてみてください。
その一呼吸で、
高階関数は「難しそうなテクニック」から、
“関数の振る舞いを再利用・拡張するための、気持ちいい設計ツール” に変わっていきます。

