ゴール:「関数の“型”を見ただけで、何をするかだいたい分かる」状態を目指す
可読性の高い関数型設計って、難しい言い方をしているけれど、やりたいことはシンプルです。
その関数の「型」だけを見たときに、
- 何を受け取って
- 何を返して
- どんな役割を持っていそうか
が、だいたいイメージできる状態を目指します。
ここでは、TypeScript の「関数の型」を、
どう書けば読みやすく・伝わりやすくなるかを、
初心者目線でかみ砕いていきます。
基本の形:関数型を「その場で書かない」勇気
その場で書くと一気に読みにくくなる例
まず、よくある「読みにくい関数型」から。
function handleUser(
fetchUser: (id: number) => Promise<{ id: number; name: string; email?: string }>,
logger: (message: string, level?: "info" | "warn" | "error") => void
): Promise<void> {
// ...
}
TypeScript動くし、型も正しいけれど、
パッと見て「何を渡せばいいのか」「何をしてくれるのか」が頭に入りづらいですよね。
ここでの問題は、
「関数の型をその場で全部書いている」ことです。
関数型に“名前”をつけるだけで一気に読みやすくなる
同じものを、こう書き換えます。
type User = {
id: number;
name: string;
email?: string;
};
type FetchUser = (id: number) => Promise<User>;
type LogLevel = "info" | "warn" | "error";
type Logger = (message: string, level?: LogLevel) => void;
function handleUser(fetchUser: FetchUser, logger: Logger): Promise<void> {
// ...
}
TypeScript関数の宣言部分だけを見ると、
- fetchUser は「ユーザーを取ってくる関数」
- logger は「ログを出す関数」
ということが、型の“形”ではなく“名前”から分かります。
可読性の高い関数型設計の第一歩は、
「関数の型に名前をつける」ことです。
関数型に名前をつけるときのコツ
「何をするか」を動詞で表す
関数型の名前は、基本的に「動詞」ベースにすると読みやすくなります。
例えば、次のような名前は直感的です。
type Predicate<T> = (value: T) => boolean; // 条件判定
type Mapper<T, U> = (value: T) => U; // 変換
type AsyncMapper<T, U> = (value: T) => Promise<U>; // 非同期変換
type Reducer<T, U> = (acc: U, value: T) => U; // 畳み込み
TypeScriptこれを使うと、関数の型が一気に読みやすくなります。
function filter<T>(items: T[], predicate: Predicate<T>): T[] {
// ...
}
function map<T, U>(items: T[], mapper: Mapper<T, U>): U[] {
// ...
}
function reduce<T, U>(items: T[], reducer: Reducer<T, U>, initial: U): U {
// ...
}
TypeScript「関数の型を見たときに、声に出して読めるか?」を基準にすると、
名前の付け方がだんだん良くなっていきます。
「役割」と「タイミング」を名前に含める
例えば、ログを出す関数でも、役割が違えば名前も変えられます。
type ErrorHandler = (error: unknown) => void;
type SuccessHandler<T> = (value: T) => void;
type CompletionHandler = () => void;
TypeScriptこれを使うと、こう書けます。
type Subscribe<T> = (
onSuccess: SuccessHandler<T>,
onError: ErrorHandler,
onComplete: CompletionHandler
) => void;
TypeScript「成功時」「失敗時」「完了時」という役割が、
型の名前だけで伝わります。
可読性の高い関数型は、
「引数の順番と名前」だけでなく、
「型の名前」からも役割が伝わるように設計されています。
ジェネリクスを使うときの“読みやすさ”の守り方
T, U, R を「意味のある型名」に変えてみる
ジェネリクスは便利ですが、
何でもかんでも T, U, R にすると、
読み手が迷子になります。
例えば、こういう型があります。
type Mapper<T, U> = (value: T) => U;
TypeScriptこれはまだシンプルですが、
もう少し具体的にすることもできます。
type UserToDtoMapper = (user: User) => UserDto;
TypeScriptあるいは、ジェネリクスを使いつつ、
型パラメータに意味を持たせることもできます。
type Mapper<Input, Output> = (value: Input) => Output;
TypeScriptこれなら、Mapper<User, UserDto> と書いたときに、
「User を UserDto に変換する関数」という意味が、
かなりはっきり伝わります。
「ジェネリクス+関数型」は“型エイリアス”に閉じ込める
例えば、こういう関数があります。
function transform<T, U>(
items: T[],
mapper: (value: T, index: number, all: T[]) => U
): U[] {
// ...
}
TypeScriptこれをそのまま読むのは、少ししんどいです。
関数型を外に出してしまいましょう。
type ArrayMapper<T, U> = (value: T, index: number, all: T[]) => U;
function transform<T, U>(items: T[], mapper: ArrayMapper<T, U>): U[] {
// ...
}
TypeScript「配列用の mapper なんだな」ということが、
型の名前から分かります。
ジェネリクスを含む関数型は、
その場で書かずに「型エイリアスに閉じ込める」と、
関数宣言がかなり読みやすくなります。
高階関数(関数を受け取る・返す)の型を読みやすくする
関数を受け取る関数の型
例えば、「条件に合う要素だけを残す」関数を考えます。
function filter<T>(items: T[], predicate: (value: T) => boolean): T[] {
// ...
}
TypeScriptこれでも悪くはないですが、
predicate の型を外に出すと、もっと読みやすくなります。
type Predicate<T> = (value: T) => boolean;
function filter<T>(items: T[], predicate: Predicate<T>): T[] {
// ...
}
TypeScript「filter は、配列と Predicate を受け取る関数だ」と、
一瞬で理解できます。
関数を返す関数の型
例えば、「ログ付きの関数を作る」高階関数を考えます。
function withLogging<TArgs extends unknown[], TResult>(
fn: (...args: TArgs) => TResult
): (...args: TArgs) => TResult {
return (...args: TArgs) => {
console.log("calling with", args);
const result = fn(...args);
console.log("result", result);
return result;
};
}
TypeScript型としては正しいですが、
宣言部分だけ見ると、かなり情報量が多いです。
ここでも「関数型に名前をつける」が効きます。
type AnyFunc<TArgs extends unknown[] = unknown[], TResult = unknown> =
(...args: TArgs) => TResult;
function withLogging<TArgs extends unknown[], TResult>(
fn: AnyFunc<TArgs, TResult>
): AnyFunc<TArgs, TResult> {
// ...
}
TypeScript「任意の関数 AnyFunc を受け取って、
同じシグネチャの関数を返すんだな」ということが、
型の名前から分かります。
高階関数の型は、
「関数型に名前をつける」ことで一気に読みやすくなります。
「可読性の高い関数型」を設計するときの視点
その1:型の“形”ではなく“名前”で意味を伝える
関数の型をその場で全部書くと、
どうしても「形」から意味を読み取る必要が出てきます。
可読性を上げたいなら、
- 引数の型
- 戻り値の型
- 関数そのものの型
に、できるだけ「意味のある名前」をつけてあげることです。
type FetchUser = (id: UserId) => Promise<User>;
type UserId = number;
TypeScriptこうしておくと、
「number だから ID かな?」ではなく、
「UserId という“ID 用の型”なんだな」と分かります。
その2:「その場で書かない」ことを恐れない
関数宣言のところで、
ジェネリクスや条件付き型を全部展開しない。
「これはこの型エイリアスに任せよう」と、
型のロジックを外に出してあげる。
type WrapResult<T, O extends WrapOptions | undefined> = /* ... */;
function wrap<T, O extends WrapOptions | undefined>(
value: T,
options?: O
): WrapResult<T, O> {
// ...
}
TypeScriptこうすると、
関数の宣言は「型レベルの関数呼び出し」のように読めます。
「WrapResult が何をしているか」は、
必要になったときにだけ見に行けばいい。
その3:「呼び出す側のコード」を頭に浮かべながら設計する
関数型の可読性は、
宣言だけでなく「呼び出し側の見え方」にも強く影響します。
例えば、次の 2 つを比べてみてください。
function process(
fn: (value: string, index: number, all: string[]) => boolean
): void {
// ...
}
function process(
predicate: Predicate<string>
): void {
// ...
}
TypeScript呼び出し側から見ると、
process((value, index, all) => { /* ... */ });
process(value => { /* ... */ });
TypeScript後者のほうが、
「これは条件判定用の関数なんだな」とすぐに分かります。
関数型を設計するときは、
「この型を使う側のコードがどう見えるか?」を
必ず一緒にイメージしてみてください。
まとめ:可読性の高い関数型設計を自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
可読性の高い関数型とは、
- その場でゴチャゴチャ書かず、型に名前をつけて外に出している
- 名前から「何をする関数か」「どんな役割か」が伝わる
- ジェネリクスや高階関数も、型エイリアスを使って細かく分けている
- 呼び出し側のコードを見たときに、関数の意図がすぐに分かる
という状態のこと。
コードを書いていて、
「この関数の型、ちょっと読みにくいな」と感じたら、
一度手を止めて、
「この関数型に名前をつけるとしたら、何て呼ぶ?」
「この引数や戻り値は、どんな“役割の型”として切り出せる?」
と自分に聞いてみてください。
その一呼吸が、
あなたの TypeScript の関数型を、
ただ“正しいだけ”の型から、
“読んだ瞬間に意図が伝わる型” に育てていきます。
