まず前提:「いつも書け」ではなく「ここぞで書く」
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戻り値型を書いていないので、result は any[] になりがちです。
(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 の型は「うるさいチェック」ではなく、
あなたの設計意図を守ってくれる相棒になっていきます。
