こんにちは!TypeScriptの型システムの世界へようこそ。
フロントエンドからバックエンドまで、コードの海をスイスイ泳げるようになるために、今日はとてもエキサイティングなテーマを用意しました。
他の言語からTypeScriptに入ってきた方や、基本構文を覚えたばかりの段階だと、「ある関数の実行結果(戻り値)を、次の関数の引数として安全にリレーしていくパイプライン処理」の型付けで手が止まってしまうことってよくありますよね。
「あっちの引数はこっちの戻り値に依存しているのに、どう型を繋げばいいんだろう……?」
安心してください。ここをクリアすれば、あなたのTypeScriptのスキルは一段上のステージに到達します。今日は、関数同士がバトンを綺麗にパスし合える「依存型」のジェネリクス設計について、優しく、そして本質的なところまで紐解いていきましょう!
—
1. なぜ「依存型(関数の連鎖)」の理解が必要なのか?
まずは、私たちがよくやりがちな「ちょっと残念なコード」から見てみましょう。
例えば、文字列を加工して、最終的に数値を返すようなパイプラインを考えます。
// よくある(でも型安全が少し不安な)例
function pipe(value: any, fn1: (arg: any) => any, fn2: (arg: any) => any) {
return fn2(fn1(value));
}
このコード、`any` だらけで何でも通ってしまうので、TypeScriptを使っている意味がほとんどありませんよね。`fn1` が何を返すのか、`fn2` が何を受け取るべきなのか、コンパイラはまったくチェックしてくれません。
私たちが目指したいのは、「1番目の関数の戻り値の型」を自動的に検出し、それが「2番目の関数の引数の型」と完璧に一致しているかを、コンパイル時にビシッと保証する世界です。
これを実現するのが、TypeScriptのジェネリクス(総称型)を巧みに使った依存型の応用テクニックになります。
—
2. 基本的なアプローチ:2つの関数を繋ぐパイプライン
まずはシンプルに、初期値と「2つの関数」を受け取って実行する `pipe2` 関数を作ってみましょう。イメージとしては、陸上競技の「リレー」です。
// T: 初期値の型
// U: 1番目の関数の戻り値(=2番目の関数の引数)の型
// V: 最終的な戻り値の型
function pipe2
initialValue: T,
fn1: (arg: T) => U,
fn2: (arg: U) => V
): V {
const intermediate = fn1(initialValue); // ここでU型が確定する
return fn2(intermediate); // ここでV型が確定する
}
コードの心臓部を読み解く
1. `
2. `fn1` は `T` を受け取り、`U` を返します。
3. `fn2` は、`fn1` の戻り値である `U` をそのまま引数の型として受け取るように縛っています(ここがポイント!)。
4. 最終的に `fn2` の戻り値である `V` が、このパイプライン全体の戻り値になります。
実際に使ってみましょう。
// 文字列を数値に変換し、さらにそれを2倍するパイプライン
const result = pipe2(
“123”, // T: string
(str) => parseInt(str, 10), // fn1: (arg: string) => number (これが U)
(num) => num 2 // fn2: (arg: number) => number (これが V)
);
// result は自動的に number 型として推論されます!
console.log(result); // 246
もしここで、`fn2` に文字列を期待するような処理を書いてしまうと……?
const errorExample = pipe2(
“123”,
(str) => parseInt(str, 10), // U は number
(num) => num.toUpperCase() // ❌ エラー!number型に toUpperCase は存在しない
);
TypeScriptのコンパイラが「おいおい、前の関数は `number` を返しているのに、次の関数は `string` を期待しているよ!」と、実行する前に赤く波線を引いて教えてくれます。これが型安全の醍醐味ですね!
—
3. 実践編:可変長の関数を繋ぐ型定義(TypeScriptの真骨頂)
「2つ繋げるのはわかったけど、実務では3つでも4つでも関数を繋ぎたいんだよね」という声が聞こえてきそうです。
ここからが少しプロフェッショナルな領域になりますが、TypeScriptの型推論の力を借りて、任意の数の関数を安全に繋ぐ仕組みを作ってみましょう。
TypeScriptでは、タプル型(要素の型と順番が固定された配列)とジェネリクスを組み合わせることで、これがエレガントに実現できます。
// 初期値を受け取り、関数配列を順番に実行していくパイプライン
// 推論を賢く働かせるための少し高度なテクニックです
// 1. 関数の型を表現するヘルパー
type UnaryFunction = (arg: any) => any;
// 2. パイプライン本体
// 最初の引数の型からスタートし、最後の関数の戻り値まで型を連鎖させます
function pipe(value: A): A;
function pipe(value: A, fn1: (arg: A) => B): B;
function pipe(value: A, fn1: (arg: A) => B, fn2: (arg: B) => C): C;
function pipe(
value: A,
fn1: (arg: A) => B,
fn2: (arg: B) => C,
fn3: (arg: C) => D
): D;
// 実装部(オーバーロードのシグネチャをまとめる)
function pipe(value: unknown, …fns: UnaryFunction[]): unknown {
return fns.reduce((acc, fn) => fn(acc), value);
}
この設計が美しい理由
関数オーバーロード(多重定義)を用いることで、開発者が関数を渡していくにつれて、TypeScriptのインテリセンス(入力補完)が「あ、今の段階のデータ型はこれだな」とリアルタイムに型をバトンタッチしていくようになります。
const processed = pipe(
“hello typescript”,
(s) => s.toUpperCase(), // s は string、戻り値は string
(s) => s.split(” “), // s は string、戻り値は string[]
(arr) => arr.length // arr は string[]、戻り値は number
);
// processed は一連の処理を追跡した結果、綺麗に `number` 型になります!
—
4. 陥りがちな文法エラーと注意点
この依存型やパイプラインを実装する際、初学者がハマりやすいポイントがいくつかあります。あらかじめ知っておくことで、無駄なハマり時間をゼロにしましょう。
罠1: `any` や `unknown` で型を放棄してしまう
「型エラーが面倒だからとりあえず `any` にしちゃえ」と逃げると、依存型の連鎖がその瞬間に途切れます。一度 `any` が混入すると、それ以降の関数の引数はすべて `any` になり、型安全のドミノ倒しが崩壊します。
→ 対策: コンパイルエラーが出たら逃げずに、どの段階で型がミスマッチを起こしているのか(どのバトンを落としたのか)をエディタのツールチップで確認しましょう。
罠2: アロー関数の型推論の方向を勘違いする
TypeScriptのジェネリクスは、原則として「左から右へ、あるいは渡された引数から戻り値へ」型を決定します。
関数をインラインで書く際、あまりに複雑に書きすぎるとコンパイラが型を推論できず、手動で型注釈(例: `(str: string) => …`)を書いてあげる必要がある場合があります。コンパイラが迷子になっていたら、一度明示的な型を与えて道案内してあげてください。
—
まとめ
今回は、関数の戻り値の型を次の引数の型として利用する「依存型」の応用について解説しました。
- 依存型を意識することで、関数と関数の間のデータ受け渡しが完全に型安全になる。
- ジェネリクス(`
` など)を使って、型を変数のように扱いながらバトンタッチしていく。 - オーバーロードなどを活用すれば、実用的な複数関数の連鎖(パイプライン)も美しく型付けできる。
ここをクリアできれば、単なる「動くコード」を書くプログラマーから、保守性が高く堅牢な設計ができる「TypeScript使い」への大きな一歩を踏み出したことになります。
ぜひ、ご自身のプロジェクトやplaygroundで実際にコードを書いて、この型推論の気持ちよさを体感してみてくださいね。それでは、次回の記事もお楽しみに!