こんにちは!フロントエンドからNode.jsまで、日々のコードと向き合ってお疲れ様です。
TypeScriptを書いていると、「あれ、この関数の引数や戻り値のパターン、どうやって型を定義するのが一番スマートなんだろう?」と悩むことってありませんか?特に、渡す値によって返ってくる値の形が変わるような場面です。
今回は、TypeScriptの関数シグネチャ設計において避けて通れない「関数オーバーロード」と「Union型(共用体型)」のどちらを選ぶべきか、その判断基準について深く掘り下げていきたいと思います。
「まだTypeScriptを始めたばかりで、どっちを使えばいいか迷っちゃうな…」という方も大丈夫。先輩エンジニアが優しく、かつ本質的な部分までしっかりガイドしますので、一緒にここをクリアしてTypeScriptの基礎をマスターしちゃいましょう!
—
1. そもそも「オーバーロード」と「Union型」って何だっけ?
まずは基本のおさらいからいきましょう。ある関数が「数値を渡したら数値を返し、文字列を渡したら文字列を返す」という仕様だったとします。
このとき、型の表現方法には主に次の2つのアプローチがあります。
1. Union型アプローチ: 引数も戻り値も「AまたはB」としてひとまとめにする。
2. オーバーロードアプローチ: 「こういう入力をしたらこう返す」というパターンを複数宣言する。
言葉だけだとイメージしにくいので、実際のコードを見ながらそれぞれの特徴を紐解いていきましょう。
—
2. ケーススタディ:IDやデータを取得する関数を作ろう
例えば、ユーザーのID(数値または文字列)を受け取って、対応するデータを返す `fetchData` という関数を考えてみます。
アプローチA:Union型でシンプルに書く
まずは、Union型を使った最も素朴な実装です。
// 引数も戻り値も Union型 (string | number) を使う
function fetchData(id: string | number): string | number {
if (typeof id === “string”) {
return `User data for string ID: ${id}`;
} else {
return 12345; // ダミーの数値データ
}
}
// 使い方と推論結果
const result1 = fetchData(“abc”); // 型は string | number になる!?
const result2 = fetchData(123); // これも型は string | number になる!?
おや? ここにUnion型の大きな弱点があります。
`fetchData(“abc”)` と文字列を渡しているのだから、返り値も絶対に `string` になってほしいですよね。しかし、上記のコードでは、TypeScriptは `result1` の型を `string | number` と推論してしまいます。
これでは、返り値に対して文字列用のメソッド(例:`.toUpperCase()` など)を使おうとしたときに、TypeScriptが「`number`かもしれないからダメ!」と怒ってしまいます。これがUnion型の限界です。
—
アプローチB:関数オーバーロードで「契約」を結ぶ
次に、関数オーバーロードを使って同じ関数を書き直してみます。オーバーロードとは、コンパイラに対して「こういう入力のときは、こういう出力になる」という対応表(契約)を複数教えるテクニックです。
// 1. シグネチャの宣言(入力と出力のペアを定義する)
function fetchData(id: string): string;
function fetchData(id: number): number;
// 2. 実体(実装シグネチャ)
function fetchData(id: string | number): string | number {
if (typeof id === “string”) {
return `User data for string ID: ${id}`;
} else {
return 12345;
}
}
// 使い方と推論結果
const resultA = fetchData(“abc”); // なんと!型は完璧に string に推論される
const resultB = fetchData(123); // こちらも完璧に number に推論される
すごいですね!オーバーロードを使うことで、`”abc”` を渡したときは `string`、`123` を渡したときは `number` と、入力と出力の結びつき(関連性)をTypeScriptに正確に伝えることができました。これぞプロの技です。
—
3. どっちを使うべき? 現場で迷ったときの「判断基準」
「じゃあ、これからは全部オーバーロードにすればいいんだ!」と思ったそこのあなた、ちょっと待ってください。
実は、オーバーロードは記述が冗長になりがちで、実装部分(実体)の型定義が複雑になるというデメリットもあります。TypeScriptのコードベースを美しく保つために、以下の判断基準を頭に入れておきましょう。
🌟 判断基準のまとめ
| 特徴・状況 | Union型が向いているケース | オーバーロードが向いているケース |
| :— | :— | :— |
| 入力と出力の連動 | 入力の型が変わっても、出力の型が常に同じ場合 | 入力の型によって、出力の型がガラリと変わる場合 |
| コードの簡潔さ | とてもシンプルに書ける | 宣言が多くなり、コードが長くなる |
| メンテナンス性 | 変更箇所が少なく、保守しやすい | 複数のシグネチャの整合性を合わせる必要がある |
💡 ざっくり言うと:
- 「引数が `A` ならば戻り値は `X`」、「引数が `B` ならば戻り値は `Y`」というように、入力と出力がタイトに結びついている場合はオーバーロードを選びましょう。
- 単に「受け入れる値のバリエーションが複数あるだけで、返るものの構造は一緒」という場合は、シンプルなUnion型で十分です。無駄なオーバーロードはコードを読みにくくしてしまいます。
—
4. 陥りがちな罠:オーバーロードの「実装シグネチャ」の扱い
最後に、初心者がよくハマるTypeScriptならではのトラップを一つご紹介します。
オーバーロードを書く際、外から見えるのは「宣言シグネチャ」ですが、実際に処理を書く関数本体には「実装シグネチャ」が1つ存在します。
// ❌ やってしまいがちなエラー
function process(val: string): string;
function process(val: number): number;
// ここで実装シグネチャの型を間違えると…
function process(val: boolean): boolean { // 怒られる!
return val;
}
コンパイラは、外から見えるオーバーロードのパターンと、一番下にある実装シグネチャの型がきちんと互換性を持っているかを厳しくチェックします。実装シグネチャは、すべてのオーバーロードを包み込める広い型(あるいはUnion型)にしておく必要がある、という点を覚えておくと安心です。
—
まとめ
いかがだったでしょうか?
- Union型は、複数の型を受け入れるシンプルなケースで大活躍する。
- 関数オーバーロードは、「入力と出力の依存関係」をTypeScriptに正しく伝え、最高の型推論を引き出すための強力な武器。
この2つの違いと使い分けが腹に落ちると、TypeScriptでコードを書くときの「お墨付きをもらっている安心感」が何倍にも跳ね上がります。
ここをクリアできれば、あなたのTypeScriptの基礎力は確実に次のステージへと進んでいますよ。ぜひ実際の開発でも意識して使ってみてくださいね。それでは、快適なTypeScriptライフを!