こんにちは!フロントエンドからNode.jsまで、TypeScriptの荒波を一緒に航海する先輩エンジニアです。
TypeScriptの学習を進めていくと、「一つの関数に対して、引数のパターンごとに違う戻り値を返したいな」という場面に出会いますよね。そんなときに使うのが関数オーバーロード(Overloads)です。
「引数によって挙動を変えるやつね、簡単簡単!」と軽い気持ちで書き始めると、なぜかコンパイラが意図しない型を選んでしまい、「あれっ?」と頭を抱えてしまうことがあります。実はこれ、オーバーロードの定義順序が深く関わっているんです。
ここをクリアすれば、TypeScriptの型推論の仕組みがグッと見えてきて、コードの信頼性が何段階も跳ね上がりますよ。一緒に本質をマスターしていきましょう!
—
1. 関数オーバーロードの基本:なぜ順序が重要なのか?
TypeScriptのオーバーロードは、C++やJavaなどのコンパイル言語とは少し毛色が違います。
Javaなどでは「厳密なシグネチャ一致」が求められますが、TypeScriptのオーバーロードは「上から順にマッチングテストを行う」という極めてシンプルなアルゴリズムで動いています。
イメージとしては、こんな感じです。
[呼び出し側] ──> 引数 “hello” を渡した!
│
▼
[上から順にチェック]
1番目のシグネチャ: マッチする? ──(NO)──> スルー
2番目のシグネチャ: マッチする? ──(YES!)─> これに決定! 🚀
そう、TypeScriptは上から順番に「この型に当てはまる?」と総当たりでチェックしていき、最初にマッチしたシグネチャを採用するという特性を持っています。だからこそ、「どの順番で定義するか」が運命の分かれ道になるのです。
—
2. 具体例で体感する「オーバーロードの罠」
では、具体的なコードでその挙動を確認してみましょう。
ここでは、「数値を渡したら数値が返り、文字列を渡したら文字列が返る」という、一見ありふれた関数を作ってみます。
まずは、やってはいけない(バグを生みやすい)書き方から見ていきましょう。
❌ 悪い例:広すぎる型を上に書いてしまった場合
// 【誤ったオーバーロードの順序】
function processValue(value: string | number): string | number; // ◀ 1番目(何でも受け入れる広い型)
function processValue(value: number): number; // ◀ 2番目(特定の型)
function processValue(value: string): string; // ◀ 3番目(特定の型)
// 実装部(すべてのオーバーロードをカバーする)
function processValue(value: string | number): string | number {
if (typeof value === “number”) {
return value 2;
}
return value.toUpperCase();
}
// — 使い方と型推論の検証 —
const result1 = processValue(10);
// 期待値: 戻り値の型は `number` になってほしい!
// 現実: 戻り値の型は `string | number` になってしまう!😭
なぜこのようなことが起きるのでしょうか?
TypeScriptのコンパイラは、`processValue(10)` というコードを見たとき、上から順にシグネチャを確認します。
1. 1番目のシグネチャ `processValue(value: string | number)` をチェック。
→ 「おっ、`10` は `string | number` に当てはまるな!よし、これに決定!」
2. 2番目以降のシグネチャは、見向きもされずにスルーされます。
結果として、せっかく細かく型を分けたはずが、一番上にある「何でもアリの広い型」にすべてが吸い込まれてしまうのです。これがオーバーロード順序の罠です。
—
⭕ 良い例:狭い型(具体的な型)を上に書く
この問題を解決するのは簡単です。ルールはたった一つ、「より具体的で狭い型を上に、一般的で広い型を下に」配置することです。
書き直してみましょう。
// 【正しいオーバーロードの順序】
function processValue(value: number): number; // ◀ 1番目(狭い・具体的な型)
function processValue(value: string): string; // ◀ 2番目(狭い・具体的な型)
function processValue(value: string | number): string | number; // ◀ 3番目(広い・フォールバック的な型)
// 実装部
function processValue(value: string | number): string | number {
if (typeof value === “number”) {
return value 2;
}
return value.toUpperCase();
}
// — 使い方と型推論の検証 —
const numResult = processValue(10); // 戻り値の型は完璧に `number` に推論される!✨
const strResult = processValue(“abc”); // 戻り値の型は完璧に `string` に推論される!✨
このように、「特殊なものから順に並べ、最後に一般的なものを置く(Specific to General)」という原則を守るだけで、TypeScriptの型推論は驚くほど美しく機能するようになります。
—
3. 現場で役立つ!さらに複雑なオーバーロードの順序
実務では、ユニオン型やオブジェクトを受け取る、もう少し複雑なケースにも遭遇します。例えば、「ID(数値)を渡したらユーザーオブジェクトが返り、名前(文字列)を検索したらユーザーの配列が返る」というAPIクライアントを想定してみましょう。
interface User {
id: number;
name: string;
}
// ❌ 曖昧なオーバーロード
// function findUser(query: string | number): User | User[];
// これを上に書くとすべてが破壊されます…
// ⭕ 正しいオーバーロード定義
function findUser(id: number): User; // 数値なら単一のUser
function findUser(keyword: string): User[]; // 文字列ならUserの配列
function findUser(query: string | number): User | User[] { // 実装用シグネチャ
if (typeof query === “number”) {
// データベースからIDで引く処理(モック)
return { id: query, name: “Taro Yamada” };
} else {
// データベースからキーワードで検索する処理(モック)
return [{ id: 1, name: query }];
}
}
// 呼び出し側の世界
const user = findUser(42); // 型は `User`
const users = findUser(“Yamada”); // 型は `User[]`
コンパイラは、私たちが渡した引数のリテラルや型を上から順に精査し、最適な戻り値を正確に私たちに返してくれます。この恩恵により、呼び出し側で余計な型アサセーション(`as User` など)を書く必要がなくなるのです。
—
まとめ
いかがでしたでしょうか? 今回のポイントをギュッと凝縮して振り返ります。
1. 上から順の総当たり: TypeScriptのオーバーロードは上から順に評価され、最初にマッチしたシグネチャが採用される。
2. 「狭い型」を上に、「広い型」を下に: 特異的な型(リテラル型やプリミティブの個別型)を上部に置き、汎用的なユニオン型などを下部に配置する。
3. 実装シグネチャは外部から隠蔽される: 最後に書く「実装用の関数シグネチャ」は、呼び出し側からは見えず、オーバーロード全体の型を包括する広めの型にしておくのが定石。
ここをクリアできれば、TypeScriptの型システムの「空気を読む力(型推論)」を完全に手懐けることができます。あなたの書くコードの安全性と開発体験は、今日から一段と素晴らしいものになりますよ。
それでは、次のステップでも一緒に楽しくTypeScriptを極めていきましょう!バッチリマスターおめでとうございます!