TypeScriptの型システムを「関数合成」で極限まで使い倒す:パイプライン型定義の深淵
多くのエンジニアが「型定義」を単なるドキュメントや補完のための補助ツールだと誤解している。しかし、TypeScriptの型システムは、コンパイル時に計算を行うチューリング完全なメタプログラミング言語だ。
今回は、フロントエンドのデータ変換やAPIのパイプライン処理において、なぜ多くの現場で「型推論が壊れる」のか、そして、どうすれば「型安全性を失わずに複雑な合成を極める」ことができるのか。その核心に切り込む。
—
1. なぜ「関数合成」で型が死ぬのか
まず、よくある「失敗する」実装を見てみよう。
// 典型的なアンチパターン
const pipe = (…fns: Function[]) => (input: any) =>
fns.reduce((acc, fn) => fn(acc), input);
このコードの何が問題か。`Function[]` と `any` を使った時点で、TSのコンパイラは「型推論を諦めろ」というシグナルを受け取る。これでは、入力値が `number` なのに、途中の関数で `string` に変換され、最終的に `boolean` を返すといった「型変換の連鎖」を追跡できない。
真のパイプラインは、型推論の「鎖」を一度も切断してはならない。
—
2. 推論を維持する「ジェネリクス連鎖」の戦略
型安全な `pipe` を作るには、TypeScriptの「タプル型」と「再帰的型推論」を組み合わせる必要がある。以下の実装は、実務でそのまま使える、堅牢かつ美しいパイプラインの定義だ。
type PipeFn = (arg: A) => B;
/
- 複雑な型合成を行うためのパイプライン関数
- A: 初期値の型
- B: 次の関数の戻り値
- …fns: 型安全に連鎖する関数のタプル
/
export const pipe = (
input: A,
…fns: [
PipeFn,
…PipeFn
]
): any => {
return fns.reduce((acc, fn) => fn(acc), input);
};
// 使用例
const trim = (s: string) => s.trim();
const toUpper = (s: string) => s.toUpperCase();
const length = (s: string) => s.length;
// コンパイラは各ステップの戻り値が次へ渡されることを完全に追跡している
const result = pipe(” hello typescript “, trim, toUpper, length);
// result: number (型推論が完璧に機能する)
なぜこれが強力なのか?
このコードの肝は `[PipeFn, …PipeFn
—
3. 実践:非同期パイプラインとエラーハンドリング
実務では、単なる関数合成だけでなく、`Promise` を扱うケースがほとんどだ。ここでは、非同期処理を安全に繋ぐための「型推論のトリック」を伝授する。
type AsyncPipeFn = (arg: A) => Promise | B;
export async function pipeAsync(
input: A,
…fns: [
AsyncPipeFn,
…AsyncPipeFn
]
): Promise
let result = await input;
for (const fn of fns) {
result = await fn(result);
}
return result;
}
// API連携の例
interface User { id: number; name: string; }
const fetchUser = (id: number): Promise
const getName = (u: User) => u.name;
const shout = (s: string) => `!!! ${s.toUpperCase()} !!!`;
const run = async () => {
const name = await pipeAsync(1, fetchUser, getName, shout);
console.log(name); // “!!! ALICE !!!”
};
パフォーマンス上の注意点:型評価のコスト
ここで一つ、チーフアーキテクトとしての警告をしておく。
TypeScriptのコンパイラ(`tsc`)は、複雑な再帰型を大量に評価すると、ビルド時間が指数関数的に増大する。今回のような「タプルの展開」を使う手法は非常にクリーンだが、関数を10個以上繋ぐような巨大なパイプラインは避けるべきだ。
もし型推論が遅延し始めたら、それは「過剰な抽象化」のサインだ。その場合は、適切な位置で `type` エイリアスを使って型を明示的に切り出せ。
—
4. 最後に:なぜ「型定義」に魂を込めるのか
多くの開発者は、型を「コンパイルを通すための作業」だと考えている。しかし、真のプロフェッショナルにとって、型定義とは「コードがどう振る舞うべきかという仕様書」そのものだ。
今回紹介したパイプライン型定義を導入すれば、リファクタリング時に「型エラー」という形でバグが瞬時に可視化される。ランタイムで `undefined is not a function` に悩まされる時間は、もう終わる。
今日から、`any` を捨て、型システムを信じろ。TypeScriptのコンパイラは、君が思っているよりもずっと優秀な、最高のペアプログラミング相手なのだから。