「関数を返す関数」って、まず何者?
いきなり型の話に行く前に、イメージを固めましょう。
function createAdder(a: number) {
return (b: number) => a + b;
}
const add10 = createAdder(10);
console.log(add10(5)); // 15
console.log(add10(20)); // 30
TypeScriptcreateAdder は「関数を返す関数」です。
呼び出した瞬間に「まだ計算はしない」。
代わりに「あとで使える“足し算専用関数”」を返している。
ここで大事なのは、
「外側の関数」と「内側の関数」を、
別々に意識して見ることです。
外側:createAdder
内側:(b: number) => number
この2段構造を、型でどう表現するかが今回のテーマです。
基本形:戻り値の型に「関数型」をそのまま書く
まずは素直に書いてみる
さっきの createAdder を、型まできっちり書くとこうなります。
function createAdder(a: number): (b: number) => number {
return (b: number) => a + b;
}
TypeScriptここでのポイントはシンプルで、
「戻り値の型に、普通の関数型 (引数) => 戻り値 をそのまま書く」
ということです。
外側の関数の型を言葉にすると、
「number を1つ受け取って、number を受け取って number を返す関数を返す関数」
になります。
一度、あえて日本語で言ってみてください。
ちょっとややこしいけど、その「ややこしさ」が構造そのものです。
もう1つシンプルな例
function createLogger(prefix: string): (message: string) => void {
return (message: string) => {
console.log(`${prefix} ${message}`);
};
}
const info = createLogger("[INFO]");
info("起動しました");
TypeScriptここでも同じ構造です。
外側:(prefix: string) => (message: string) => void
内側:(message: string) => void
「外側は“設定を受け取る関数”、内側は“実際に使う関数”」
という役割分担になっています。
関数を返す関数の「設計の意味」
「設定」と「実行」を分ける
関数を返す関数は、だいたいこういう役割を持ちます。
外側の関数:
「設定・前準備・コンテキストを受け取る」
内側の関数:
「実際の処理を行う(設定を使いながら)」
さっきの createLogger で言うと、
外側:prefix を受け取る(設定)
内側:message を受け取ってログを出す(実行)
この分離ができると、こういう書き方が自然にできます。
const info = createLogger("[INFO]");
const error = createLogger("[ERROR]");
info("起動しました");
error("失敗しました");
TypeScript「設定済みの関数を量産する」イメージですね。
ここでの重要ポイントは、
関数を返す関数は、
「設定を閉じ込めた“カスタマイズ済み関数”を作るための型」
だと捉えることです。
関数を返す関数の型を「type」で表現してみる
外側と内側を分けて名前をつける
毎回 (b: number) => number と書くのがつらくなってきたら、
型に名前をつけてしまうのが定石です。
type Adder = (b: number) => number;
function createAdder(a: number): Adder {
return (b) => a + b;
}
TypeScriptこうすると、
「createAdder は Adder を返すんだな」
「Adder は number を受け取って number を返す関数なんだな」
と、一気に読みやすくなります。
もう一歩進めて、外側も型にしてみます。
type Adder = (b: number) => number;
type AdderFactory = (a: number) => Adder;
const createAdder: AdderFactory = (a) => (b) => a + b;
TypeScriptここまで来ると、
Adder:実際に足し算する関数AdderFactory:Adder を作る関数
という「役割」が型名に乗ってきます。
関数を返す関数を設計するときは、
「外側の関数」と「内側の関数」に名前をつけてあげると、頭が一気に整理される
と思ってください。
ジェネリクスで「どんな関数でも返せる」型にする
「T を受け取って U を返す関数を作る関数」
例えば、「変換関数を作る関数」を考えます。
type Transformer<T, U> = (input: T) => U;
function createLoggedTransformer<T, U>(
fn: Transformer<T, U>
): Transformer<T, U> {
return (input: T) => {
console.log("input:", input);
const result = fn(input);
console.log("result:", result);
return result;
};
}
TypeScriptここでは、
Transformer<T, U>:T を U に変換する関数createLoggedTransformer:その Transformer を受け取って、ログ付きの Transformer を返す関数
という構造になっています。
型だけ抜き出すと、
<T, U>(fn: (input: T) => U) => (input: T) => U
TypeScriptです。
使ってみます。
const toLength = (s: string) => s.length;
const loggedToLength = createLoggedTransformer(toLength);
const len = loggedToLength("hello");
// input: hello
// result: 5
TypeScriptここでの重要ポイントは、
「ジェネリクスを使うと、“どんな T, U の変換関数でもラップできる関数”を型安全に書ける」
ということです。
関数を返す関数の型は、
「外側のジェネリクス」と「内側の関数型」がどうつながるか
を意識すると、すっと理解できます。
もう一段:カリー化(引数を分割して関数を返す)
「引数を分けて受け取る」関数の型
関数を返す関数の典型例として、「カリー化」があります。
function add(a: number, b: number): number {
return a + b;
}
function curryAdd(a: number): (b: number) => number {
return (b: number) => a + b;
}
TypeScriptadd(1, 2) を curryAdd(1)(2) のように分割して呼べるようにするイメージです。
型としては、
(a: number) => (b: number) => number
TypeScriptです。
もう少しジェネリックにすると、こうも書けます。
type Curried2<A, B, R> = (a: A) => (b: B) => R;
const curry =
<A, B, R>(fn: (a: A, b: B) => R): Curried2<A, B, R> =>
(a: A) =>
(b: B) =>
fn(a, b);
TypeScriptここでのポイントは、
「関数を返す関数の型は、“引数を分割して受け取る形”を表現するのにも使える」
ということです。
(a: A, b: B) => R を(a: A) => (b: B) => R に変換する、という発想ですね。
関数を返す関数の型を見るときのコツ
「外側」と「内側」を分けて読む
例えば、次の型を見てみます。
function withRetry<F extends (...args: any[]) => Promise<any>>(
fn: F,
maxRetry: number
): (...args: Parameters<F>) => Promise<Awaited<ReturnType<F>>> {
// 実装は省略
throw new Error("not implemented");
}
TypeScript一見ゴツいですが、分解するとこうです。
外側の関数の型:
<F extends (...args: any[]) => Promise<any>>(
fn: F,
maxRetry: number
) => (...args: Parameters<F>) => Promise<Awaited<ReturnType<F>>>
TypeScript内側で返している関数の型:
(...args: Parameters<F>) => Promise<Awaited<ReturnType<F>>>
TypeScriptつまり、
「Promise を返す関数 F を受け取って、
同じ引数で呼べて、同じ結果を返すけど“リトライ機能付き”の関数を返す」
という設計です。
関数を返す関数の型が複雑に見えたら、
必ずこう分けて読んでください。
外側:何を受け取って、どんな“関数”を返すのか
内側:返される“関数”は、どんな引数・戻り値を持っているのか
この二段階で見る癖をつけると、
どんなに長い型でも、ちゃんと意味として読めるようになります。
まとめ:関数を返す関数の型を自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
関数を返す関数は、
「外側の関数は“設定・前準備”を受け取り、
内側の関数は“実際の処理”を行う」
という二段構造を持っていて、
型としては
(外側の引数) => (内側の引数) => 戻り値
TypeScriptの形で表現できる。
シンプルな場合は、戻り値の型にそのまま関数型を書く。
複雑になってきたら、内側・外側の関数型に type 名をつけて整理する。
ジェネリクスを使うと、「どんな関数でもラップできる枠」を型安全に作れる。
コードを書くとき、
「これは“設定を受け取って、カスタマイズ済みの関数を返す”形にできないか?」
と一度考えてみてください。
その一呼吸で、
関数を返す関数は「難しそうな構文」から、
“設定と実行をきれいに分離するための、強力な設計パターン” に変わっていきます。
