こんにちは!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
たったこれだけです!この `UnaryFn
—
3. 型推論を壊さない「パイプライン関数」を作ってみよう
それでは、2つの関数を左から右へ順に適用する `pipe2` 関数を作ってみましょう。
実装コード例
// 単項関数の型エイリアスを活用
type UnaryFn
/
- 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
// 2. 数値を2倍にして文字列メッセージにする関数 : (arg: number) => string
const doubleToMessage: UnaryFn
// 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
const getUpperName: UnaryFn
// 型が確定しているため、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
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
- シンプルで可読性が高く、関数の合成と相性抜群。
2. 型推論が壊れる原因は「入力の型の喪失」
- 引数の型(文脈)が得られない状態でインラインアロー関数を使うと `any` にフォールバックしてしまう。
3. データファーストな設計または明示的な型エイリアス定義で解決!
- データを起点にするか、あらかじめ `UnaryFn` で型付けした関数を渡すことで、型推論のチェーンを維持できる。
型システムの裏側にある「コンパイラがどう型を辿っていくか」という視点を持つと、 TypeScriptのエラーメッセージが急に親切なヒントに見えてくるから不思議ですよね。
ここをクリアできれば、TypeScriptでの関数型アプローチや高度な型設計の基本はバッチリマスターですよ!ぜひご自身のプロダクトのコードでも、型安全なパイプライン処理を試してみてくださいね。応援しています!