「戻り値で型が決まる」ってどういうことか
ふつうは「引数の型 → 戻り値の型」という流れで関数を考えますが、
TypeScript では 「戻り値の書き方や宣言のしかたが、逆に“型”を決める・変化させる」 場面がいくつかあります。
ここでは特に初心者が押さえておきたい
- 戻り値からの「型推論」
- 戻り値の書き方で変わる「リテラル型 vs 広い型」
- 戻り値の型で分岐の型が変わる「ユーザー定義型ガード(is)」
- 条件分岐の網羅性チェックに効く
never
あたりを、丁寧にかみ砕いていきます。
ケース1: 戻り値から型が推論される(戻り値型を書かない場合)
関数の中身から「戻り値型」が決まる
戻り値型を省略すると、TypeScript は return の中身から戻り値の型を推論します。
function add(a: number, b: number) {
return a + b;
}
TypeScriptここでは戻り値型を書いていませんが、a + b が number なので、
// 推論された型イメージ
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
TypeScriptname によって中身が変わるので、具体的なリテラルではなく「一般的な 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 ではない");
}
}
TypeScriptif (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("一般ユーザーです");
}
}
TypeScriptisAdmin の戻り値型 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 };
TypeScriptdefault に渡る shape の型は { kind: "triangle"; ... } になり、
それを assertNever(shape) に渡した瞬間、
Type ‘{ kind: “triangle”; base: number; height: number; }’ is not assignable to type ‘never’.
というエラーになります。
ここでも、
- 関数
assertNeverの「戻り値型」「引数型」の設計 areaのdefaultでのreturn assertNever(shape);
という「戻り値」の書き方が、
「型が漏れを検出できるかどうか」に直結しているわけです。
まとめ:戻り値が「型の振る舞い」を決めているポイント
初心者のうちは「引数に型をつける」ことに意識が行きがちですが、
TypeScript の面白さが出てくるのは、むしろ 「戻り値側の設計」 です。
ざっくり整理すると:
- 戻り値型を省略すると、
returnの中身から型が決まる(型推論) - 戻り値をリテラルっぽく書くかどうかで、「狭い型」か「広い型」かが変わる
value is 型の形式を使うと、「その関数を通ったあとの変数の型」が変わる(型ガード)neverを戻り値に持つ関数と組み合わせると、「ありえないはずのパターン」をエラーにできる
どれも共通しているのは、
「この関数を呼んだあと、周りの世界(型)はどう変わっているべきか?」
という問いです。
戻り値側を設計するときは、ぜひそこまで含めてイメージしてみてください。
単に「何を返すか」ではなく、
「何を返す“ことにする”ことで、呼び出し側の型や振る舞いをどう変えたいのか」 まで考え始めたとき、
TypeScript の型設計が一段レベルアップします。
