こんにちは!フロントエンドからNode.jsまで、TypeScriptの荒波を日々乗りこなしているシニアアーキテクトの先輩です。
今回は、TypeScriptの学習において多くの人が「おっ、これ凄いわ!」と感動するポイントであり、型システムの真骨頂である「条件付き型(Conditional Types)を用いた引数による戻り値の動的変換」について解説していきますね。
「関数の引数に渡す値によって、返ってくる戻り値の型をガラリと変えたい」――そんな要件に直面したとき、あなたならどう書きますか?
従来のプログラミング言語にあるような「関数のオーバーロード」を使うのも手ですが、Conditional Typesを使いこなせるようになると、コードの美しさと型安全性が劇的に跳ね上がります。
ここをクリアすれば、TypeScriptの基本はバッチリマスターできますよ。一緒に優しく、深く、紐解いていきましょう!
—
1. なぜ「オーバーロード」だけでは物足りないのか?
まずは、私たちがよくやりがちな従来の関数オーバーロード(型定義の重ね合わせ)を見てみましょう。例えば、「入力が文字列なら文字列を返し、数値なら数値を返す」という関数を作りたいとします。
// 従来のオーバーロードによるアプローチ
function processValue(value: string): string;
function processValue(value: number): number;
function processValue(value: string | number): string | number {
if (typeof value === “string”) {
return value.toUpperCase();
}
return value 2;
}
const result1 = processValue(“hello”); // 型は string
const result2 = processValue(42); // 型は number
これでも動きますよね。でも、もし扱いたい型が「文字列」「数値」「真偽値」「配列」「オブジェクト」……と増えていったらどうでしょう? オーバーロードのシグネチャを何行も書き並べるのは、メンテナンスの観点から見ても少しナンセンスです。
ここで登場するのが、Conditional Types(条件付き型)です。
—
2. Conditional Typesの基本構文:三項演算子の型バージョン
Conditional Typesの基本形は、JavaScriptの三項演算子(`cond ? true_path : false_path`)によく似ています。
T extends U ? X : Y
これは、「もし型 `T` が型 `U` に代入可能(extends)ならば型 `X`、そうでなければ型 `Y` になりなさい」という、コンパイラへの命令です。型の世界の `if文` だと思ってください。
これを関数に応用すると、「引数の型 `T` が何であるかに応じて、戻り値の型を動的に決定する」という神業が可能になります。
—.
3. 実践:引数の型で戻り値を変えるスマートな関数
では実際に、Conditional Typesを使って、オーバーロードを使わずにスッキリと書かれたモダンな関数を実装してみましょう。
/
- 渡された値の型に応じて処理を変え、適切な型を返すスマートな関数
/
function smartProcess
value: T
): T extends string ? string : T extends number ? number : boolean {
if (typeof value === “string”) {
// 実行時の処理
return value.trim() as any;
} else if (typeof value === “number”) {
return (value 10) as any;
} else {
return (!value) as any;
}
}
// — 利用側のコード(コンパイラがどう型を推論するか)—
const strResult = smartProcess(” typescript “);
// 戻り値の型は “string”
const numResult = smartProcess(100);
// 戻り値の型は “number”
const boolResult = smartProcess(false);
// 戻り値の型は “boolean”
コードのここがポイント!
1. ジェネリクス(`
`T` というプレースホルダー(型変数)を使うことで、TypeScriptは「今、実際にどんな型の値が渡されたか」を正確に記憶します。
2. 戻り値の型で条件分岐を行う
`T extends string ? string : …` 部分で、`T` が文字列なら `string`、数値なら `number`……というように、戻り値の型をピンポイントで決定しています。
—
4. 陥りやすい罠:「型アサーション (`as any`)」の誘惑と向き合う
ここで、鋭い皆さんはこう思ったかもしれません。
「あれ、関数の内部(return文)で `as any` を使って型エラーを無理やりねじ伏せていない?」
その通りです。ここに、初心者が必ずと言っていいほどハマる「TypeScriptの壁」があります。
なぜ内部で型エラーになるのか?
TypeScriptのコンパイラは非常に慎重です。関数の内部で `T extends string ? …` という条件付き型の評価をするとき、コンパイラは「この関数の中身を実行している時点では、`T` が最終的にどの型になるかまだ確定しきれていない」と判断します。そのため、`return value` と素直に書くと、「本当にこの戻り値の型と一致しているのか証明できないよ!」と怒られてしまうのです。
安全に乗り切るためのベストプラクティス
ライブラリのコア部分などで厳密性を担保したい場合は、関数をオーバーロードと条件付き型を組み合わせて隠蔽するか、ジェネリクスのオーバーロードを適切に利用します。
しかし、一般的なアプリケーション開発においては、関数の出口(return部分)で適切にキャスト(アサーション)するか、あるいは次項で紹介する「ジェネリックなオーバーロード」を使い分けるのが実務的です。
—
5. さらに実践的:ジェネリックなオーバーロードとの使い分け
Conditional Typesは強力ですが、複雑にしすぎるとコンパイル速度が落ちたり、エディタのツールチップ(ホバー時の型表示)が読みにくくなったりします。
- シンプルな分岐(2〜3パターン): オーバーロードのほうが読みやすい場合もある。
- 複雑な動的変換や、入力型と出力型が連動する汎用的なユーティリティ関数: Conditional Typesの独壇場。
例えば、以下のような「配列を受け取ったら最初の要素を返し、単体ならそのまま返す」というユーティリティは、Conditional Typesの最も美しい使い方の一つです。
// 配列なら要素の型、単体ならそのままの型を返す
type Unwrap
function parseInput
if (Array.isArray(input)) {
return input[0] as Unwrap
}
return input as Unwrap
}
const a = parseInput([1, 2, 3]); // 型は number
const b = parseInput(“hello”); // 型は string
(※ `infer` というキーワードが出てきましたが、これは「型から特定のパーツをパズルのように引き剥がす」ための高度な機能です。これについても、また別の機会に深く解説しますね!)
—
まとめ
今回は、関数型における Conditional Types を用いた「引数の型による戻り値の動的変換」について解説しました。
- 引数の型を `T` として受け取り、戻り値の型で `T extends X ? Y : Z` を使うことで、動的な型変化を実現できる。
- オーバーロードよりもスッキリ書ける反面、関数の実装内部ではコンパイラの性質上、型アサーションが必要になるケースがある。
- 使い所を見極めれば、型安全でメンテナンス性の高い、プロフェッショナルなコードを書くことができる。
最初は少し難しく感じるかもしれませんが、型を「自由に組み立てるパズル」のように捉えられるようになると、TypeScriptを書くのが何倍も楽しくなりますよ。
日々のコーディングで、ぜひこのテクニックを取り入れてみてくださいね。それでは、次のレクチャーでお会いしましょう!