TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – 可読性の高い関数型設計

TypeScript TypeScript
スポンサーリンク

ゴール:「関数の“型”を見ただけで、何をするかだいたい分かる」状態を目指す

可読性の高い関数型設計って、難しい言い方をしているけれど、やりたいことはシンプルです。

その関数の「型」だけを見たときに、

  • 何を受け取って
  • 何を返して
  • どんな役割を持っていそうか

が、だいたいイメージできる状態を目指します。

ここでは、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 の関数型を、
ただ“正しいだけ”の型から、
“読んだ瞬間に意図が伝わる型” に育てていきます。

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