TypeScriptを掌握せよ:型推論を止めない「関数パイプライン」の極意
こんにちは。TypeScriptの深淵を覗き込み、その型システムと日々対話しているアーキテクトです。
TypeScriptを触っていると、「型エイリアス(Type Alias)」と「インターフェース(Interface)」のどちらを使うべきか迷う瞬間がありますよね。結論から言えば、「オブジェクトの形状を定義するならInterface、計算や合成といった『型変換』を定義するならType Alias」が、現代のTypeScriptにおける最適解です。
今日はその中でも、特に現場のコードを美しく、かつ強固にする「関数パイプラインの型定義」に焦点を当てましょう。ここを突破すれば、あなたのコードは「ただ動くもの」から「型安全という守護神に守られた美しい設計」へと進化します。
—
1. なぜ「パイプライン」で型が壊れるのか?
関数を合成して `f(g(h(x)))` のように書くのは読みづらいですよね。これを `pipe(x, h, g, f)` のように流れるように書きたいと思うのは当然の欲求です。
しかし、単純に `(arg: any) => any` と定義してしまうと、TypeScriptの魂である「型推論」が死んでしまいます。
// 悪い例:anyに逃げると、パイプの途中でどんな型が流れているか誰も分からなくなる
type PipeFn = (arg: any) => any;
const pipe = (val: any, …fns: PipeFn[]) => fns.reduce((acc, fn) => fn(acc), val);
これでは、`x` が `number` なのか `string` なのか、コンパイラは一切教えてくれません。私たちが目指すべきは、「入力の型が次の関数の引数に正しく継承され、最終的な出力まで型が追跡される状態」です。
—
2. 型推論を維持する「ジェネリクス連鎖」の設計
型推論を止めないためには、関数合成の過程を「型レベルのバケツリレー」のように定義する必要があります。
具体的には、タプル型とジェネリクスを組み合わせます。
/
- パイプラインの型定義
- A: 初期の入力型
- B: 次の関数の出力型
/
type UnaryFn = (arg: A) => B;
// 2つの関数を合成する最小単位の型エイリアス
type Pipe2 = (
arg: A,
fn1: UnaryFn,
fn2: UnaryFn
) => C;
const pipe2: Pipe2 = (arg, fn1, fn2) => fn2(fn1(arg));
// 試してみましょう
const add1 = (n: number) => n + 1;
const toString = (n: number) => `Result: ${n}`;
const result = pipe2(10, add1, toString);
// result の型は自動的に string になる!素晴らしいですね。
ここがポイント!
- `UnaryFn` を定義することで、「Aを受け取ってBを返す関数」という役割を明確にしました。
- `Pipe2` の中で `fn1` の戻り値の型 `B` が、そのまま `fn2` の引数の型 `B` として受け渡されています。これが型推論の連鎖です。
—
3. 陥りやすい罠:なぜ推論が効かないのか?
よくあるエラーとして、関数を配列で受け取ろうとして `UnaryFn
// 陥りやすいNG例
const pipe = (val: any, fns: ((arg: any) => any)[]) => { … }
配列にしてしまうと、TypeScriptは「配列の中にある複数の関数の型を個別に追跡する」という能力を失います。 TypeScriptの型システムは、配列の中身を「同質の要素の集合」とみなすため、個別の変換ステップを無視してしまうのです。
もし複数の関数を扱いたい場合は、可変長引数(Rest Parameters)を使いましょう。
// 可変長引数を使った高度な型定義の例
const pipe =
return fns.reduce((val, fn) => fn(val), initial);
};
※厳密に型安全なN個の関数合成を行うには、再帰的な型定義が必要になりますが、まずはこの「型を捨てない」という意識を持つだけで、あなたのコードの品質は劇的に向上します。
—
結論:TypeScriptは「型という地図」を描く作業
関数パイプラインを型定義することは、単なるテクニックではありません。それは「データがどのように変容し、どの型にたどり着くか」という地図を、コンパイラと共有する作業です。
- Interface は、システムが持つ「静的な形状(名刺)」を定義する。
- Type Alias は、システムで行われる「動的な変換(計算式)」を定義する。
この使い分けをマスターすれば、あなたはもうTypeScriptの初学者ではありません。型システムという強力な相棒と共に、複雑なロジックをシンプルに構築できるはずです。
「ここが少し難しかったかな?」と感じたら、まずは `Pipe2` の定義を自分で書き写して、VSCodeのホバー機能で型がどう変化しているか確認してみてください。コンパイラが教えてくれる型情報のヒントが、あなたのコードを導いてくれるはずですよ。
それでは、また次回の深淵でお会いしましょう。ハッピーコーディング!