まず「関数型エイリアス」とは何かをはっきりさせる
関数型エイリアスは、かんたんに言うと
「よく使う関数の“形”に名前をつけること」
です。
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;
TypeScriptfilter 用の関数型:
type Predicate<T> = (value: T, index: number, array: T[]) => boolean;
TypeScriptreduce 用の関数型:
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 など)
を表現するのに向いている。
型に切り出すかどうかは、
「同じ形が何度も出てくるか」
「その関数の役割に名前をつけた方が読みやすいか」
を基準に決める。
コードを書くとき、
「この関数の“形”には、名前をつけてあげた方が気持ちいいかな?」
と一度立ち止まってみてください。
その一呼吸で、
関数型エイリアスは「ただの省略記法」から、
設計意図をコードに刻み込むための、大事な道具 に変わっていきます。
