TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – 関数を返す関数の型

TypeScript
スポンサーリンク

「関数を返す関数」って、まず何者?

いきなり型の話に行く前に、イメージを固めましょう。

function createAdder(a: number) {
  return (b: number) => a + b;
}

const add10 = createAdder(10);
console.log(add10(5));  // 15
console.log(add10(20)); // 30
TypeScript

createAdder は「関数を返す関数」です。

呼び出した瞬間に「まだ計算はしない」。
代わりに「あとで使える“足し算専用関数”」を返している。

ここで大事なのは、

「外側の関数」と「内側の関数」を、
別々に意識して見ることです。

外側: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

こうすると、

createAdderAdder を返すんだな」
Addernumber を受け取って number を返す関数なんだな」

と、一気に読みやすくなります。

もう一歩進めて、外側も型にしてみます。

type Adder = (b: number) => number;
type AdderFactory = (a: number) => Adder;

const createAdder: AdderFactory = (a) => (b) => a + b;
TypeScript

ここまで来ると、

Adder:実際に足し算する関数
AdderFactoryAdder を作る関数

という「役割」が型名に乗ってきます。

関数を返す関数を設計するときは、
「外側の関数」と「内側の関数」に名前をつけてあげると、頭が一気に整理される
と思ってください。


ジェネリクスで「どんな関数でも返せる」型にする

「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;
}
TypeScript

add(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 名をつけて整理する。
ジェネリクスを使うと、「どんな関数でもラップできる枠」を型安全に作れる。

コードを書くとき、
「これは“設定を受け取って、カスタマイズ済みの関数を返す”形にできないか?」
と一度考えてみてください。

その一呼吸で、
関数を返す関数は「難しそうな構文」から、
“設定と実行をきれいに分離するための、強力な設計パターン” に変わっていきます。

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