TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – 関数型エイリアス設計

TypeScript
スポンサーリンク

まず「関数型エイリアス」とは何かをはっきりさせる

関数型エイリアスは、かんたんに言うと

「よく使う関数の“形”に名前をつけること」

です。

type StringToNumber = (value: string) => number;

const length: StringToNumber = (value) => value.length;
const toNumber: StringToNumber = (value) => Number(value);
TypeScript

(value: string) => number という「関数の型」に、
StringToNumber という名前をつけています。

これをちゃんと設計できるようになると、

「このプロジェクトでよく出てくる“関数の形”」
「この関数に渡してほしい“コールバックの形”」

を、コードのあちこちで一貫して表現できるようになります。


基本:関数型エイリアスは「関数の役割」に名前をつけるもの

「ただの短縮形」ではなく「意味を持ったラベル」

例えば、次の2つを比べてみてください。

type F1 = (value: string) => void;
type LogHandler = (value: string) => void;
TypeScript

どちらも型としては同じですが、
読みやすさは圧倒的に LogHandler の方が上です。

F1 は「何をする関数なのか」が分かりません。
LogHandler は「ログを扱うハンドラなんだな」と一瞬で伝わります。

関数型エイリアスを設計するときの基本は、

「短く書きたいから型に名前をつける」のではなく、
「この関数の“役割”を名前で表現したいから型にする」

という発想です。

だから、
Fn, Callback, Handler1 みたいな名前は、
できるだけ避けた方がいいです。

「何のための関数か」が分かる名前をつける。
ここが、関数型エイリアス設計の一番大事なポイントです。


コールバック用の関数型エイリアスを設計してみる

「同じ形のコールバック」が増えたら、まず型にする

例えば、メッセージを扱う処理がいくつかあるとします。

function onMessage(handler: (message: string) => void) {
  // ...
}

function onError(handler: (message: string) => void) {
  // ...
}

function onDebug(handler: (message: string) => void) {
  // ...
}
TypeScript

毎回 (message: string) => void と書いていると、
「全部同じ形なのに、コピペだらけ」になります。

ここで関数型エイリアスの出番です。

type MessageHandler = (message: string) => void;

function onMessage(handler: MessageHandler) {
  // ...
}

function onError(handler: MessageHandler) {
  // ...
}

function onDebug(handler: MessageHandler) {
  // ...
}
TypeScript

こうすると、

「この3つの関数は、どれも MessageHandler を受け取るんだな」
MessageHandler は“メッセージを処理する関数”なんだな」

と、コードの意図が一気に読みやすくなります。

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

const logToConsole: MessageHandler = (message) => {
  console.log(message);
};

onMessage(logToConsole);
onError(logToConsole);
onDebug(logToConsole);
TypeScript

「同じ形のコールバックが2回以上出てきたら、型エイリアスを検討する」
くらいの感覚を持っておくと、設計がきれいにまとまっていきます。


ジェネリクス付き関数型エイリアスで「パターン」を表現する

「T を受け取って U を返す関数」という“型のパターン”

例えば、「何かを変換する関数」というパターンを表したいとします。

type Transformer<T, U> = (input: T) => U;
TypeScript

これは、

「T を受け取って U を返す関数」

という“形”に名前をつけたものです。

これを使うと、いろんな関数の型を共通化できます。

const stringToNumber: Transformer<string, number> = (value) => Number(value);
const numberToString: Transformer<number, string> = (value) => value.toString();
TypeScript

さらに、高階関数にも使えます。

function withLog<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 に変換する関数なんだな」と分かります。


map / filter / reduce 風の関数型エイリアスを設計する

「よく出てくる関数の形」を型として固定する

配列処理でよく出てくる関数の形も、
関数型エイリアスにしておくと便利です。

map 用の関数型:

type MapFn<T, U> = (value: T, index: number, array: T[]) => U;
TypeScript

filter 用の関数型:

type Predicate<T> = (value: T, index: number, array: T[]) => boolean;
TypeScript

reduce 用の関数型:

type Reducer<T, A> = (acc: A, value: T, index: number, array: T[]) => A;
TypeScript

これを使って、自作の map / filter / reduce を書いてみます。

function myMap<T, U>(array: T[], fn: MapFn<T, U>): U[] {
  const result: U[] = [];
  for (let i = 0; i < array.length; i++) {
    result.push(fn(array[i], i, array));
  }
  return result;
}

function myFilter<T>(array: T[], fn: Predicate<T>): T[] {
  const result: T[] = [];
  for (let i = 0; i < array.length; i++) {
    if (fn(array[i], i, array)) {
      result.push(array[i]);
    }
  }
  return result;
}

function myReduce<T, A>(array: T[], fn: Reducer<T, A>, initial: A): A {
  let acc = initial;
  for (let i = 0; i < array.length; i++) {
    acc = fn(acc, array[i], i, array);
  }
  return acc;
}
TypeScript

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

「関数型エイリアスを使うことで、
“この引数は map 用の関数”“これは条件用の関数”と、
役割が一目で分かるようになる」

ということです。

ただの (value: T) => U ではなく、
MapFn<T, U>Predicate<T> という名前が付くことで、
コードの意図がぐっと伝わりやすくなります。


関数型エイリアスを「インターフェース」として使うパターン

オブジェクトの中に「関数の役割」をまとめる

関数型エイリアスは、単体で使うだけでなく、
「オブジェクトのプロパティとしての関数」にも使えます。

例えば、ある処理のライフサイクルを表すコールバック群。

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

ここでは、
onStart, onSuccess, onError それぞれに「関数型」が付いています。

これを受け取る関数はこう書けます。

function runTask(callbacks: TaskCallbacks) {
  callbacks.onStart?.();

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

ここでのポイントは、

「関数型エイリアスは、
“オブジェクトの中の関数たち”の設計にも使える」

ということです。

TaskCallbacks という名前を見ただけで、
「このオブジェクトはタスクのコールバック群なんだな」と分かります。


関数型エイリアス設計の「判断基準」を持っておく

どんなときに「型に切り出すべきか」を決める

関数型エイリアスを乱発すると、
逆に「型の名前だらけで分かりにくい」状態にもなりえます。

なので、自分なりの基準を持っておくと楽です。

例えば、こんな問いを自分に投げてみてください。

「この関数の形は、コードの中に2回以上出てきているか?」
「この関数の形は、このプロジェクトにとって“意味のある役割”を持っているか?」
「この関数の型に名前をつけたら、関数の意図が読みやすくなるか?」

どれか1つでも「はい」と思ったら、
関数型エイリアスに切り出す価値が高いです。

逆に、

その場限りの小さな無名関数
map の中の一行コールバック
一度しか出てこない特殊な関数

などは、無理に型エイリアスにしなくても構いません。

大事なのは、

「短くしたいから」ではなく、
「意味をはっきりさせたいから」型エイリアスにする

という姿勢です。


まとめ:関数型エイリアス設計を自分の言葉で言うと

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

関数型エイリアスは、

「よく使う“関数の形”に、意味のある名前をつけるもの」

であり、

コールバックの形(MessageHandler, Predicate など)
変換の形(Transformer<T, U> など)
map / filter / reduce 用の関数型(MapFn, Predicate, Reducer など)
ライフサイクル用のコールバック群(TaskCallbacks など)

を表現するのに向いている。

型に切り出すかどうかは、

「同じ形が何度も出てくるか」
「その関数の役割に名前をつけた方が読みやすいか」

を基準に決める。

コードを書くとき、
「この関数の“形”には、名前をつけてあげた方が気持ちいいかな?」
と一度立ち止まってみてください。

その一呼吸で、
関数型エイリアスは「ただの省略記法」から、
設計意図をコードに刻み込むための、大事な道具 に変わっていきます。

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