ゴール:「if や switch で分岐した結果、戻り値の型がどう変わるか」を意識して設計できるようになる
関数の戻り値って、
「常に同じ型」だけじゃなくて、条件によって“中身の型”が変わることがあります。
- 引数が
stringならstringを返す - 引数が
numberならnumberを返す - 成功なら
Success、失敗ならFailureを返す
こういうときに、
- 関数の「宣言上の戻り値型」をどう書くか
- 関数の中の
if/switchと、戻り値型の関係をどう設計するか
を理解しておくと、
「TypeScript に怒られないように書く」から
「TypeScript に“意図を伝えながら”書く」に一段上がれます。
基本パターン:ユニオン型をそのまま戻り値にする
条件分岐で「どちらかの型」を返す関数
まずは一番素直なパターンから。
type Success = {
ok: true;
value: number;
};
type Failure = {
ok: false;
error: string;
};
function parse(text: string): Success | Failure {
const n = Number(text);
if (Number.isNaN(n)) {
return {
ok: false,
error: "not a number",
};
}
return {
ok: true,
value: n,
};
}
TypeScriptここでのポイントはシンプルです。
- 関数の戻り値型:
Success | Failure(ユニオン型) - 関数の中:
ifの条件によってSuccessかFailureのどちらかを返す
呼び出し側は、戻り値を見て分岐します。
const result = parse("123");
if (result.ok) {
// ここでは result は Success 型
console.log(result.value * 2);
} else {
// ここでは result は Failure 型
console.error(result.error);
}
TypeScriptif (result.ok) の条件によって、
TypeScript が result の型を Success / Failure に絞り込んでくれます。
ここで大事なのは、
「関数の戻り値型にユニオンを宣言し、
関数の中の条件分岐と“対応する形”で値を返す」
という設計パターンを、まず1つ持っておくことです。
もう一歩:ジェネリクス+条件付き戻り値(conditional types)
「引数の型によって戻り値の型を変えたい」
例えば、こんな関数を考えます。
- 引数が
stringなら、stringを返したい - 引数が
numberなら、numberを返したい
実装は「そのまま返すだけ」だとしても、
戻り値の型をちゃんと変えたい。
function identity(value: string | number): string | number {
return value;
}
TypeScriptこれだと、呼び出し側でこうなります。
const a = identity("hello"); // a: string | number
TypeScript本当は string だけなのに、string | number になってしまう。
ここで出てくるのが「条件付き型(conditional types)」です。
type Identity<T> = T extends string ? string : number;
TypeScriptこれは、
Tがstringならstring- それ以外なら
number
という意味の型です。
これを関数に組み合わせると、こう書けます。
function smartIdentity<T extends string | number>(value: T): T {
return value;
}
TypeScriptこの例では conditional type は使っていませんが、
「ジェネリクスの型パラメータ T をそのまま戻り値に使う」ことで、
const a = smartIdentity("hello"); // a: string
const b = smartIdentity(123); // b: number
TypeScriptという「引数に応じて戻り値型が変わる」挙動を実現しています。
ここでの重要ポイントは、
「戻り値型を T にしておけば、
条件分岐の中で T に合う値を返す限り、
TypeScript は“引数に応じて戻り値型が変わる”と理解してくれる」
ということです。
条件分岐と「戻り値の型の一貫性」
すべての分岐で「宣言した戻り値型」に合う値を返す
例えば、こういう関数を考えます。
function toNumberOrNull(text: string): number | null {
if (text === "") {
return null;
}
return Number(text);
}
TypeScript戻り値型は number | null。if の中では null、それ以外では number を返しています。
これは OK です。
でも、こう書くとどうなるでしょう。
function bad(text: string): number | null {
if (text === "") {
return null;
}
// return "hello"; // エラー: string は number | null に代入できない
}
TypeScriptif の外で string を返そうとすると、
宣言した戻り値型 number | null に合わないのでエラーになります。
当たり前に見えますが、
「条件分岐のどのパスでも、最終的に“宣言した戻り値型”に合う値を返す必要がある」
というルールを、常に意識しておくのが大事です。
すべてのパスで「必ず何かを返す」ことも重要
もう1つ、よくあるエラーがこれです。
function maybe(text: string): number | null {
if (text === "") {
return null;
}
// ここで何も返していない
}
TypeScriptTypeScript は「この関数は number | null を返すと宣言しているのに、if の外のパスで何も返していない」と怒ります。
条件分岐で戻り値が変わる関数では、
- どのパスでも「宣言した戻り値型」に合う値を返す
- どのパスでも「必ず何かを返す」
この2つを満たすように設計する必要があります。
「条件によって戻り値の“中身”を変える」設計パターン
Result 型パターン(成功 / 失敗)
さっきの Success | Failure を、もう少し汎用的にします。
type Ok<T> = {
ok: true;
value: T;
};
type Err<E> = {
ok: false;
error: E;
};
type Result<T, E> = Ok<T> | Err<E>;
TypeScriptこれを使って、こういう関数を設計できます。
function parseJson(text: string): Result<unknown, string> {
try {
const value = JSON.parse(text);
return {
ok: true,
value,
};
} catch (e) {
return {
ok: false,
error: "invalid json",
};
}
}
TypeScript呼び出し側:
const result = parseJson('{"a": 1}');
if (result.ok) {
// result: Ok<unknown>
console.log(result.value);
} else {
// result: Err<string>
console.error(result.error);
}
TypeScriptここでの設計のポイントは、
- 戻り値型は「成功か失敗か」のユニオン
- 関数の中の条件分岐(try/catch)と、戻り値のバリアントが対応している
- 呼び出し側は
okを見て、型が自動で絞り込まれる
という流れがきれいに繋がっていることです。
「条件分岐による戻り値型の変化」を、
“ユニオン型+判定用フラグ”という形で設計する
のは、実務でもかなりよく使うパターンです。
条件付き型(conditional types)と戻り値の関係を少しだけ
「T が〇〇なら △△ を返す」という型レベルの条件分岐
もう少し踏み込んで、型レベルの if 文=条件付き型を見てみます。
type ToPromise<T> = T extends Promise<any> ? T : Promise<T>;
TypeScriptこれは、
TがすでにPromise<…>なら、そのままT- そうでなければ
Promise<T>
という意味の型です。
これを戻り値型に使うと、こういう関数が書けます。
function ensurePromise<T>(value: T): ToPromise<T> {
if (value instanceof Promise) {
// ここでは value: Promise<unknown> だが、型的には T extends Promise<any>
return value as ToPromise<T>;
}
return Promise.resolve(value) as ToPromise<T>;
}
TypeScript呼び出し側:
const p1 = ensurePromise(123); // p1: Promise<number>
const p2 = ensurePromise(Promise.resolve(1)); // p2: Promise<number>
TypeScriptここは少し難しいですが、
言いたいことは1つです。
「条件分岐によって戻り値の“型”を変えたいとき、
ジェネリクス+条件付き型を組み合わせると、
“引数の型に応じて戻り値型が変わる関数”を設計できる」
ということです。
最初からここを完璧に理解する必要はありません。
「そういう設計の方向性がある」と知っておくだけで十分です。
実務的な設計の視点
1. 戻り値型を「ユニオン」で表すか、「ジェネリクス+条件」で表すか
関数の戻り値が条件で変わるとき、
大きく2つのアプローチがあります。
- 単純に
A | Bのようなユニオン型にする - ジェネリクス+条件付き型で「T に応じて変わる」ようにする
前者はシンプルで読みやすい。
後者は柔軟だけど、少し難しい。
初心者のうちは、まずユニオン型で十分です。
function parse(text: string): Success | Failure { ... }
TypeScript「引数の型に応じて戻り値型を変えたい」
「ライブラリ的な汎用関数を書きたい」
となってきたら、
ジェネリクス+条件付き型を検討する、くらいの順番でいいです。
2. 条件分岐と戻り値型の“対応関係”を意識する
関数の中に if / switch が出てきたら、
こう自分に問いかけてみてください。
- この分岐ごとに、どんな型の値を返している?
- それらを全部まとめると、関数の戻り値型は何になる?
- その戻り値型を、関数の宣言にちゃんと書けている?
この「対応関係」が崩れると、
TypeScript に怒られたり、
読み手が「この関数、何を返すの?」と迷子になります。
逆に言うと、
「分岐ごとの戻り値の型」と「宣言上の戻り値型」が
きれいに対応している関数は、それだけで設計が美しい
です。
まとめ:条件分岐による戻り値型変化を自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
関数の戻り値は、if や switch の条件によって「中身の型」が変わることがある。
そのとき、
- シンプルな場合は、戻り値型をユニオン(
A | B)で表現し、
分岐ごとにAかBを返すように設計する。 - 呼び出し側では、そのユニオンをフラグ(
okなど)で判定して、
TypeScript の絞り込み(narrowing)を活かす。 - もっと汎用的に「引数の型に応じて戻り値型を変えたい」ときは、
ジェネリクス+条件付き型を使う。
コードを書くとき、
「この if / switch のそれぞれのパスで、どんな型の値を返している?」
「それを全部まとめたものが、関数の戻り値型として宣言されている?」
と一度立ち止まってみてください。
その一呼吸で、
条件分岐は「ただのロジック」から、
“型ときれいに噛み合った、意図の見える設計” に変わっていきます。
