【入門編】Type Aliasで定義する「関数型プログラミング」の合成パターン – TypeScript コア・型システムの基礎解析バイブル

こんにちは!TypeScriptの世界へようこそ。

今回は、TypeScriptの型システムにおける超重要テーマである「型エイリアス(Type Alias)」を使った関数型プログラミングの合成パターンについてお話しします。

「高階関数」や「パイプライン処理」と聞くと、少し難しそうに感じるかもしれません。しかし、TypeScriptコンパイラが「関数をどう見ているか(型をどう推論しているか)」という仕組みさえ押さえてしまえば、決して怖くありませんよ。

この記事を読み終わる頃には、型推論を破綻させずにエレガントなパイプラインコードを書くテクニックがしっかり身についているはずです。「ここをクリアすれば、TypeScriptの基本はバッチリマスターできますよ!」という気持ちで丁寧に解説していきますね。

—

1. なぜ「Interface」ではなく「Type Alias」で関数型を定義するのか?

TypeScriptで型を定義するとき、`interface` と `type` のどちらを使うべきか迷ったことはありませんか?
結論から言うと、単一の関数シグネチャを定義する場合は `type` (Type Alias) を使うのが圧倒的にお勧めです。

見比べてみよう:2つの定義方法

// ① Interface による関数型の定義(呼び出し可能オブジェクト)
interface ExecuterInterface {
(input: string): number;
}

// ② Type Alias による関数型の定義
type ExecuterType = (input: string) => number;

どちらも「`string` を受け取って `number` を返す関数」を表していますが、視認性の違いは一目瞭然ですよね。

型エイリアス(`type`)は、アロー関数 `(arg: A) => B` とまったく同じ直感的なメンタルモデルで記述できます。関数型プログラミングでは、関数を「値」として扱い、次々に合成していくため、この軽量で素直な記述ができる `type` が非常に相性が良いのです。

—

2. 関数の合成(Composition)とパイプライン(Pipeline)の基本イメージ

まずは「関数を合成する」とはどういうことか、イメージ図で掴んでおきましょう。

データ `A` を関数 `f` に渡して `B` を得て、その `B` を関数 `g` に渡して `C` を得る。この一連の流れを1つの新しい関数 `h` としてまとめる操作が「関数合成」です。

【関数のパイプライン処理のイメージ】

Data (A) —> [ 関数 f: (A) => B ] —> Data (B) —> [ 関数 g: (B) => C ] —> Data (C)
========================================================================================
===> これらを 1 つに繋げた新関数 [ pipe(f, g): (A) => C ] を作る!

このデータの流れを型安全に構築するために、まずは最小単位となる「単項関数(引数を1つだけ取る関数)」の型エイリアスを定義してみましょう。

/

  • 汎用的な単項関数を表す型エイリアス
  • T: 入力型のジェネリクス
  • R: 出力(戻り値)型のジェネリクス

/
type UnaryFn = (arg: T) => R;

たったこれだけです!この `UnaryFn` が、すべての高階関数・パイプライン処理の最小ビルディングブロックになります。

—

3. 型推論を壊さない「パイプライン関数」を作ってみよう

それでは、2つの関数を左から右へ順に適用する `pipe2` 関数を作ってみましょう。

実装コード例

// 単項関数の型エイリアスを活用
type UnaryFn = (arg: T) => R;

/

  • 2つの関数を合成する pipe2 関数
  • 1. fn1: (a: A) => B
  • 2. fn2: (b: B) => C
  • 戻り値: (a: A) => C

/
function pipe2(
fn1: UnaryFn,
fn2: UnaryFn
): UnaryFn {
return (initialValue: A): C => {
return fn2(fn1(initialValue));
};
}

実際に使ってみる(型推論が完璧に機能する例)

// 1. 文字列の長さを取得する関数 : (arg: string) => number
const getLength: UnaryFn = (str) => str.length;

// 2. 数値を2倍にして文字列メッセージにする関数 : (arg: number) => string
const doubleToMessage: UnaryFn = (num) => `Double is ${num 2}`;

// 3. pipe2 で合成!
// TypeScriptは A=string, B=number, C=string と自動で型推論してくれます
const processString = pipe2(getLength, doubleToMessage);

// 実行してみましょう
const result = processString(“Hello”);
console.log(result); // 出力: “Double is 10”

TypeScriptコンパイラは、`pipe2` に渡された `getLength` の型情報から `A = string`, `B = number` を読み取り、続く `doubleToMessage` の型情報から `C = string` であることを自動で見つけ出して(推論して)くれています。美しいですよね!

—

4. 陥りやすい罠!なぜ型推論が壊れてしまうのか?

一見完璧に見える関数合成ですが、実務で書いていると「あれ?急に引数の型が `any` や `unknown` になって型エラー(TS2345)が出る…」という現象に必ずぶち当たります。

初学者の方が最も陥りやすい罠とその原因を解説しますね。

罠:インラインで無名関数(アロー関数)を渡すと型推論が崩れる?

// 渡す関数をその場で(インラインで)書こうとすると…
const processBad = pipe2(
(str) => str.length, // ❌ エラー! Error: Parameter ‘str’ implicitly has an ‘any’ type.
(num) => num 2
);

なぜこれが起きるのか?(コンパイラの気持ちになってみよう)

TypeScriptの型推論には「文脈的型付け(Contextual Typing)」という仕組みがあります。
TypeScriptコンパイラは、関数を評価するとき「左から右」へと型情報を流していきます。

1. 第1引数の `(str) => str.length` を見に行きます。
2. しかし、`pipe2` のジェネリクス `A` が何なのか、入力データがまだ渡されていないため手がかりが一切ありません。
3. `A` が不明なため、`str` の型も決められず、コンパイラは「型がわかりません(Implicit any)」とお手上げ状態になってしまうのです。

【型推論の行き詰まり】

pipe2( (str) => str.length , (num) => num 2 )
▲
└─ Aの型情報がどこにも見当たらないため、str の型が推論できない!

—

5. 型推論を絶対に壊さないための「3つの処方箋」

この型推論の破綻を防ぐために、私たちが現場で使っているプロのテクニックを3つ紹介しますね。

処方箋①:入力値を評価の起点にする(データファーストなpipe)

関数を合成した「関数を返す」のではなく、「初期値(データ)」を第1引数として直接受け取るスタイルの `pipe` に設計を変更します。

/

  • データを第1引数に取る pipe 関数

/
function pipeData(
data: A,
fn1: UnaryFn,
fn2: UnaryFn
): C {
return fn2(fn1(data));
}

// 使うとき:第1引数に “TypeScript” (string) があるため、コンパイラは A = string と確定できる!
const result = pipeData(
“TypeScript”,
(str) => str.length, // str は自動的に string と推論される!
(len) => `Length: ${len}` // len は自動的に number と推論される!
);

console.log(result); // 出力: “Length: 10”

データ(値)を起点にすることで、型情報が `A (string)` $\rightarrow$ `fn1` $\rightarrow$ `B (number)` $\rightarrow$ `fn2` $\rightarrow$ `C (string)` と左から右へスムーズに伝播します!

—

処方箋②:型エイリアスであらかじめ明示的な型をつける

合成したい処理をインラインで書かず、最初に `UnaryFn` を使って型エイリアスを割り当てた独立した関数として定義しておきます。

type User = { id: number; name: string };

// 型エイリアスを明記して定義
const fetchUser: UnaryFn = (id) => ({ id, name: “Alice” });
const getUpperName: UnaryFn = (user) => user.name.toUpperCase();

// 型が確定しているため、pipe2 にそのまま渡しても推論は一切壊れない
const getUserUpperName = pipe2(fetchUser, getUpperName);

const name = getUserUpperName(1); // name は string 型
console.log(name); // 出力: “ALICE”

—

処方箋③:可変長引数(3つ以上の合成)に対応するオーバーロード定義

実務では2つだけでなく、3つ、4つと関数を繋げたくなりますよね。
TypeScriptでこれを型安全に行うには、型エイリアスと関数オーバーロードを組み合わせるのが最も確実で美しい方法です。

// 3つの合成に対応するオーバーロード定義
function pipe(fn1: UnaryFn): UnaryFn;
function pipe(fn1: UnaryFn, fn2: UnaryFn): UnaryFn;
function pipe(
fn1: UnaryFn,
fn2: UnaryFn,
fn3: UnaryFn
): UnaryFn;

// 実装(内部処理は配列の reduce で回す)
function pipe(…fns: UnaryFn[]): UnaryFn {
return (initialValue: any) =>
fns.reduce((acc, fn) => fn(acc), initialValue);
}

// — 試してみましょう —
const addExclamation = (s: string): string => `${s}!`;
const toLength = (s: string): number => s.length;
const isEven = (n: number): boolean => n % 2 === 0;

// 3つの異なる型を経由するパイプライン
const checkEvenLength = pipe(
addExclamation, // string -> string
toLength, // string -> number
isEven // number -> boolean
);

const isLengthEven = checkEvenLength(“Hi”); // Hi! -> 3 -> false
console.log(isLengthEven); // 出力: false

—

6. まとめ:今回の重要ポイント

お疲れ様でした!今回は型エイリアスを使った関数型プログラミングの基礎と、その型推論メカニズムについて解説しました。

最後に、今回の重要ポイントを振り返っておきましょう。

1. 関数型の定義は `type UnaryFn = (arg: T) => R` が基本!

  • シンプルで可読性が高く、関数の合成と相性抜群。

2. 型推論が壊れる原因は「入力の型の喪失」

  • 引数の型(文脈)が得られない状態でインラインアロー関数を使うと `any` にフォールバックしてしまう。

3. データファーストな設計または明示的な型エイリアス定義で解決!

  • データを起点にするか、あらかじめ `UnaryFn` で型付けした関数を渡すことで、型推論のチェーンを維持できる。

型システムの裏側にある「コンパイラがどう型を辿っていくか」という視点を持つと、 TypeScriptのエラーメッセージが急に親切なヒントに見えてくるから不思議ですよね。

ここをクリアできれば、TypeScriptでの関数型アプローチや高度な型設計の基本はバッチリマスターですよ!ぜひご自身のプロダクトのコードでも、型安全なパイプライン処理を試してみてくださいね。応援しています!

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