TypeScript | 基礎文法:関数の基礎 – 戻り値で型が決まるケース

TypeScript
スポンサーリンク

「戻り値で型が決まる」ってどういうことか

ふつうは「引数の型 → 戻り値の型」という流れで関数を考えますが、
TypeScript では 「戻り値の書き方や宣言のしかたが、逆に“型”を決める・変化させる」 場面がいくつかあります。

ここでは特に初心者が押さえておきたい

  • 戻り値からの「型推論」
  • 戻り値の書き方で変わる「リテラル型 vs 広い型」
  • 戻り値の型で分岐の型が変わる「ユーザー定義型ガード(is)」
  • 条件分岐の網羅性チェックに効く never

あたりを、丁寧にかみ砕いていきます。


ケース1: 戻り値から型が推論される(戻り値型を書かない場合)

関数の中身から「戻り値型」が決まる

戻り値型を省略すると、TypeScript は return の中身から戻り値の型を推論します。

function add(a: number, b: number) {
  return a + b;
}
TypeScript

ここでは戻り値型を書いていませんが、a + bnumber なので、

// 推論された型イメージ
function add(a: number, b: number): number;
TypeScript

として扱われます。

使う側ではちゃんと number として扱えます。

const result = add(1, 2);
// result は number 型として推論される
TypeScript

戻り値に string を返すように書けば、戻り値型は string になります。

function greet(name: string) {
  return `Hello, ${name}`;
}
// → 戻り値型は string と推論される
TypeScript

戻り値型を書いていないときは、「関数の中で何を return しているか」が、そのまま型を決めると思ってください。

複数の return があるときは「全部の型の合成」になる

function getStatus(code: number) {
  if (code === 200) {
    return "ok";
  } else if (code === 404) {
    return "not_found";
  } else {
    return null;
  }
}
TypeScript

この場合、TypeScript は

  • 1つ目の return: "ok"
  • 2つ目の return: "not_found"
  • 3つ目の return: null

を全部まとめて、戻り値型を

"ok" | "not_found" | null
TypeScript

と推論します。

つまり 「すべての分岐で返しうる型のユニオン」が戻り値型になる わけです。

この性質は便利ですが、あとから分岐を変えたときに「意図しない型」に広がることもあるので、
重要な関数はあえて ): "ok" | "not_found" | null のように戻り値型を明示した方が安全なことも多いです。


ケース2: 戻り値の“書き方”で、「狭い型」か「広い型」かが変わる

リテラル型として扱われる場合

function yes() {
  return "yes";
}
TypeScript

このままでも TypeScript は賢くて、戻り値型を "yes"(リテラル型)と推論してくれる場合が多いです。

const v = yes(); 
// v: "yes"
TypeScript

「この関数は必ず "yes" という文字列しか返さない」という型になります。

“広い” string 型として扱われる場合

一方で、もっと複雑な関数になると、戻り値型はただの string に広がりやすいです。

function makeMessage(name: string) {
  return `Hello, ${name}`;
}
// 戻り値型: string
TypeScript

name によって中身が変わるので、具体的なリテラルではなく「一般的な string」として扱われる、という感覚です。

戻り値型を“固定したい”ときは型を書く

たとえば「この関数は絶対 "ON""OFF" しか返さない」と決めたい場合。

function getSwitchLabel(isOn: boolean): "ON" | "OFF" {
  return isOn ? "ON" : "OFF";
}
TypeScript

こう書くと、戻り値は必ず "ON" | "OFF" のどちらかであることが型として保証されます。
あとからうっかり "YES" を返そうとすると、即エラーになります。

「戻り値のパターンを型として絞り込みたいとき」は、推論任せにせずに戻り値型を書いてしまうのがおすすめです。


ケース3: 戻り値の型で「呼び出し側の型」が変わる(ユーザー定義型ガード)

ここがいちばん「戻り値で型が決まる」という感覚を強く実感できるところです。

基本パターン:value is 型 という戻り値型

ユーザー定義型ガードという機能を使うと、関数の戻り値型によって「if 文の中の型」が変わります。

function isString(value: unknown): value is string {
  return typeof value === "string";
}
TypeScript

この value is string が特別です。

普通なら (): boolean と書くところを、
「boolean ではなく“型の条件”として使える形」で書いています。

この関数を if 文で使うと:

function printLength(value: unknown) {
  if (isString(value)) {
    // ここでは value は string 型に絞られる
    console.log(value.length);
  } else {
    // ここでは value は string 以外(unknown のままなど)
    console.log("string ではない");
  }
}
TypeScript

if (isString(value)) が true のブロックでは、TypeScript は
isString の戻り値が true なら、value is string という条件が満たされている」と理解し、
value の型を string に絞り込んでくれます。

つまり、

  • 定義時:value is string という「戻り値の型」を宣言する
  • 呼び出し時:if 文の中でその宣言を使って「変数の型」が変わる

という流れです。
「戻り値の型が、その関数を使ったときの“型の振る舞い”を変える」典型例です。

複合的な型の絞り込みにも使える

type User = { type: "user"; name: string };
type Admin = { type: "admin"; name: string; permissions: string[] };
type Person = User | Admin;

function isAdmin(person: Person): person is Admin {
  return person.type === "admin";
}
TypeScript

これを使うと:

function printPermissions(person: Person) {
  if (isAdmin(person)) {
    // ここでは person は Admin 型に絞られる
    console.log(person.permissions);
  } else {
    // ここでは person は User 型
    console.log("一般ユーザーです");
  }
}
TypeScript

isAdmin の戻り値型 person is Admin があるおかげで、
if 文の中で person.permissions に安全にアクセスできます。

「“is〜” 系関数の戻り値型をうまく設計すると、呼び出し側の型推論が一気に賢くなる」
これがユーザー定義型ガードの気持ちいいところです。


ケース4: never を戻り値に持つ関数と、網羅性チェック

戻り値が never の関数は「絶対に返ってこない」

function fail(message: string): never {
  throw new Error(message);
}
TypeScript

この関数は、呼び出されたら例外を投げて終わるので、「正常な戻り値」が存在しません。
そのため戻り値型は never になります。

never 自体の話はすでに触れていると思うので、ここでは never を使うことで条件分岐の漏れをチェックできる」という文脈に絞ります。

never と組み合わせると「ありえないパターン」が型エラーになる

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number };

function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${JSON.stringify(x)}`);
}

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    default:
      return assertNever(shape);
  }
}
TypeScript

ここで assertNever の引数型が x: never になっています。
default に渡される shape が本当に「ありえない」場合だけ通る設計です。

もし Shape に新しいバリエーションを追加して、switch に書き忘れると:

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number }
  | { kind: "triangle"; base: number; height: number };
TypeScript

default に渡る shape の型は { kind: "triangle"; ... } になり、
それを assertNever(shape) に渡した瞬間、

Type ‘{ kind: “triangle”; base: number; height: number; }’ is not assignable to type ‘never’.

というエラーになります。

ここでも、

  • 関数 assertNever の「戻り値型」「引数型」の設計
  • areadefault での return assertNever(shape);

という「戻り値」の書き方が、
「型が漏れを検出できるかどうか」に直結しているわけです。


まとめ:戻り値が「型の振る舞い」を決めているポイント

初心者のうちは「引数に型をつける」ことに意識が行きがちですが、
TypeScript の面白さが出てくるのは、むしろ 「戻り値側の設計」 です。

ざっくり整理すると:

  • 戻り値型を省略すると、return の中身から型が決まる(型推論)
  • 戻り値をリテラルっぽく書くかどうかで、「狭い型」か「広い型」かが変わる
  • value is 型 の形式を使うと、「その関数を通ったあとの変数の型」が変わる(型ガード)
  • never を戻り値に持つ関数と組み合わせると、「ありえないはずのパターン」をエラーにできる

どれも共通しているのは、

「この関数を呼んだあと、周りの世界(型)はどう変わっているべきか?」

という問いです。

戻り値側を設計するときは、ぜひそこまで含めてイメージしてみてください。
単に「何を返すか」ではなく、
「何を返す“ことにする”ことで、呼び出し側の型や振る舞いをどう変えたいのか」 まで考え始めたとき、
TypeScript の型設計が一段レベルアップします。

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