TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – 条件分岐による戻り値型変化

TypeScript
スポンサーリンク

ゴール:「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 の条件によって SuccessFailure のどちらかを返す

呼び出し側は、戻り値を見て分岐します。

const result = parse("123");

if (result.ok) {
  // ここでは result は Success 型
  console.log(result.value * 2);
} else {
  // ここでは result は Failure 型
  console.error(result.error);
}
TypeScript

if (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

これは、

  • Tstring なら 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 に代入できない
}
TypeScript

if の外で string を返そうとすると、
宣言した戻り値型 number | null に合わないのでエラーになります。

当たり前に見えますが、
「条件分岐のどのパスでも、最終的に“宣言した戻り値型”に合う値を返す必要がある」
というルールを、常に意識しておくのが大事です。

すべてのパスで「必ず何かを返す」ことも重要

もう1つ、よくあるエラーがこれです。

function maybe(text: string): number | null {
  if (text === "") {
    return null;
  }
  // ここで何も返していない
}
TypeScript

TypeScript は「この関数は 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 に怒られたり、
読み手が「この関数、何を返すの?」と迷子になります。

逆に言うと、

「分岐ごとの戻り値の型」と「宣言上の戻り値型」が
きれいに対応している関数は、それだけで設計が美しい

です。


まとめ:条件分岐による戻り値型変化を自分の言葉で言うと

最後に、あなた自身の言葉でこう整理してみてください。

関数の戻り値は、
ifswitch の条件によって「中身の型」が変わることがある。

そのとき、

  • シンプルな場合は、戻り値型をユニオン(A | B)で表現し、
    分岐ごとに AB を返すように設計する。
  • 呼び出し側では、そのユニオンをフラグ(ok など)で判定して、
    TypeScript の絞り込み(narrowing)を活かす。
  • もっと汎用的に「引数の型に応じて戻り値型を変えたい」ときは、
    ジェネリクス+条件付き型を使う。

コードを書くとき、
「この if / switch のそれぞれのパスで、どんな型の値を返している?」
「それを全部まとめたものが、関数の戻り値型として宣言されている?」

と一度立ち止まってみてください。

その一呼吸で、
条件分岐は「ただのロジック」から、
“型ときれいに噛み合った、意図の見える設計” に変わっていきます。

タイトルとURLをコピーしました