TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – 戻り値型を明示すべき場面

TypeScript
スポンサーリンク

まず前提:「いつも書け」ではなく「ここぞで書く」

TypeScript は戻り値型をかなりうまく推論してくれます。
だから「全部に : 型 を書け」という話ではありません。

大事なのは、

「推論に任せると“危ない”場面」
「推論に任せると“読みづらい”場面」

を見分けて、そこだけは 意識的に戻り値型を明示する ことです。

ここから、その「ここぞ」のパターンを、具体例で整理していきます。


パターン1:外部に公開する関数(モジュールの“顔”)

ライブラリ的な関数は「型が仕様」になる

例えば、こんな関数を別ファイルから export するとします。

export function parseUser(json: string) {
  const obj = JSON.parse(json);
  return {
    id: obj.id,
    name: obj.name ?? "名無しさん",
  };
}
TypeScript

戻り値型を書いていないので、TypeScript は中身から推論します。

// 実際にはこんな型が付く(簡略化)
export function parseUser(json: string): {
  id: any;
  name: string;
}
TypeScript

ここで問題なのは、

id の型が any になっているかもしれない
将来中身を変えたときに、外から見える型も勝手に変わってしまう

という点です。

外部に公開する関数は、
「この関数はこういうものです」という“約束”を、型として固定したい 場面です。

だから、こう書く方が良いです。

type User = {
  id: string;
  name: string;
};

export function parseUser(json: string): User {
  const obj = JSON.parse(json);
  return {
    id: String(obj.id),
    name: obj.name ?? "名無しさん",
  };
}
TypeScript

これなら、

戻り値型は常に User
中の実装を変えても、外から見える型は変わらない

という状態になります。

「外から呼ばれる関数」「モジュールの“顔”になる関数」は、
戻り値型を明示して“仕様をロックする” のが TypeScript らしい書き方です。


パターン2:複雑な処理で「推論結果が読みにくい」とき

中身がごちゃごちゃしている関数ほど、戻り値型を先に決める

例えば、こんな関数を考えます。

function buildState(flag: boolean, value?: string) {
  if (!flag) {
    return { status: "idle" as const };
  }
  if (!value) {
    return { status: "loading" as const };
  }
  if (value === "error") {
    return { status: "error" as const, error: "エラーです" };
  }
  return { status: "success" as const, data: value };
}
TypeScript

これでも TypeScript はそれなりに型を推論してくれますが、
読む側からすると「この関数、結局何を返すの?」が一瞬で分かりません。

こういうときは、先に「戻り値の型」を決めてしまう方がスッキリします。

type State =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "error"; error: string }
  | { status: "success"; data: string };

function buildState(flag: boolean, value?: string): State {
  if (!flag) {
    return { status: "idle" };
  }
  if (!value) {
    return { status: "loading" };
  }
  if (value === "error") {
    return { status: "error", error: "エラーです" };
  }
  return { status: "success", data: value };
}
TypeScript

こうすると、

この関数は State を返す
State がどんなバリエーションを持つかも一目で分かる

という状態になります。

「中身を読まないと戻り値の形が分からない関数」になりそうなら、
戻り値型を明示して“ゴール”を先に見せる
のが良いです。


パターン3:union や null を返す関数(失敗の可能性がある)

「本当は失敗するかも」を型に隠さない

例えば、ユーザーを検索する関数。

function findUserName(id: number) {
  const user = db.find(id);
  if (!user) {
    return null;
  }
  return user.name;
}
TypeScript

戻り値型を書いていないと、TypeScript は string | null と推論してくれます。
でも、呼び出し側からは「パッと見でそれが分からない」ことが多いです。

こういう関数は、あえて戻り値型を明示した方が、
「この関数は null を返すことがある」という事実が目に飛び込んでくる ようになります。

function findUserName(id: number): string | null {
  const user = db.find(id);
  if (!user) {
    return null;
  }
  return user.name;
}
TypeScript

あるいは、成功/失敗を union で返す場合も同じです。

type Found = { ok: true; name: string };
type NotFound = { ok: false };

type FindResult = Found | NotFound;

function findUserName(id: number): FindResult {
  // ...
}
TypeScript

「失敗する可能性がある」「パターンが複数ある」関数は、
戻り値型を明示して“危険性”や“分岐の必要性”を型で伝える のが大事です。


パターン4:ジェネリクスを使う関数(型パラメータをコントロールしたい)

推論に任せると「広がりすぎる」ことがある

例えば、配列を変換するジェネリック関数。

function mapArray<T, U>(array: T[], fn: (value: T) => U) {
  const result = [];
  for (const item of array) {
    result.push(fn(item));
  }
  return result;
}
TypeScript

戻り値型を書いていないので、resultany[] になりがちです。
noImplicitAny などの設定次第でエラーにもなります)

ここは素直に戻り値型を明示した方がいい場面です。

function mapArray<T, U>(array: T[], fn: (value: T) => U): U[] {
  const result: U[] = [];
  for (const item of array) {
    result.push(fn(item));
  }
  return result;
}
TypeScript

こうすると、

この関数は「T[] を受け取って U[] を返す」
というジェネリックな契約がはっきりします。

ジェネリクスを使う関数では、
「型パラメータ T, U と戻り値型の関係」を自分でコントロールするために、戻り値型を明示する のが基本です。


パターン5:アロー関数を変数に入れて再利用するとき

「その変数はどういう関数なのか」をはっきりさせる

例えば、こういうコード。

const format = (value: string) => `[${value}]`;
TypeScript

これでも型推論は効きますが、
この format を別の場所に渡したり、
インターフェースの一部として使ったりするなら、
型をはっきりさせておく方が読みやすくなります。

type Formatter = (value: string) => string;

const format: Formatter = (value) => `[${value}]`;
TypeScript

ここでは、戻り値型は Formatter の中に含まれています。

アロー関数に直接 : string と書いてもいいですが、
「関数の型を名前付きで定義して、それを使う」 のは、
関数設計をきれいにするうえでとても強いパターンです。

この場合も、「戻り値型を明示する」というより、
「関数全体の型(引数+戻り値)を明示する」 という感覚で捉えてください。


パターン6:将来の変更で「戻り値の意味」が変わりそうな関数

仕様を固定しておきたいときの“保険”として

例えば、最初はこういう関数を書いたとします。

function getConfig() {
  return {
    debug: true,
    apiEndpoint: "https://example.com",
  };
}
TypeScript

しばらくして、「設定項目を増やしたい」となり、
中身をこう変えました。

function getConfig() {
  return {
    debug: true,
    apiEndpoint: "https://example.com",
    timeout: 5000,
  };
}
TypeScript

推論に任せていると、
戻り値型も勝手に「timeout 付き」に変わります。

それ自体は悪くないのですが、
「外から見たときに、どこまでを“公式な仕様”として保証するのか」が曖昧になります。

もし「この関数は debug と apiEndpoint だけを保証したい。timeout は内部用のオマケ」
という設計にしたいなら、戻り値型を明示しておくべきです。

type Config = {
  debug: boolean;
  apiEndpoint: string;
};

function getConfig(): Config {
  return {
    debug: true,
    apiEndpoint: "https://example.com",
    // timeout は返さない or 別の経路で扱う
  };
}
TypeScript

「この関数の戻り値は、将来どう変わりうるか?」を考えたとき、
“ここまでは固定したい”と思うなら、戻り値型を明示して仕様をロックする。

これも、戻り値型を明示すべき大事な場面です。


まとめ:「戻り値型を明示すべきか?」を判断する質問

最後に、判断のための問いを自分用に持っておくと楽です。

この関数は、外部(別ファイル・別モジュール)から呼ばれる“顔”になっているか?
この関数の戻り値は、失敗や複数パターンを含んでいて、呼び出し側に「ちゃんと意識してほしい」ものか?
中身を読まないと戻り値の形が分からないくらい、処理が複雑になっていないか?
ジェネリクスや union を使っていて、「推論任せだと不安」な感じがしないか?
将来の変更で、戻り値の意味が勝手に変わってほしくないか?

どれか一つでも「はい」と感じたら、
その関数は 戻り値型を明示しておく価値が高い と思っていいです。

逆に、
「その場限りの小さな処理」「ローカルなアロー関数」「map の中の一行」
みたいなところは、推論に任せてしまって構いません。

大事なのは、

「戻り値型を明示するかどうか」を、
“なんとなく”ではなく、“設計の一部として意識的に選ぶ”こと。

その感覚が育ってくると、
TypeScript の型は「うるさいチェック」ではなく、
あなたの設計意図を守ってくれる相棒になっていきます。

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