こんにちは。テックリードの私だ。
コードレビューをしていると、ジュニアやミドルクラスのエンジニアが書いたアロー関数で、次のようなコードにしばしば遭遇する。
// 一見、何の問題もないように見えるアロー関数
const fetchUserData = (userId: string) =>
fetch(`/api/users/${userId}`).then(res => res.json());
君は、この関数が返す「戻り値の型」が何になるか、コンパイラがどう型評価しているかを即座に答えられるだろうか?
「`Promise
今日は、アロー関数で戻り値の型を明示しないことで発生する「推論の罠」の正体を暴き、プロダクションコードで絶対に破綻しない堅牢な設計パターンを伝授しよう。
—
なぜ「推論への依存」は悪なのか?
TypeScriptの型推論エンジンは強力だ。だが、強力ゆえに「開発者が意図した型」ではなく、「コードから機械的に導き出せる最も広い(あるいは狭い)型」を勝手に決定してしまう。
上記の `fetchUserData` を例に取ろう。
`res.json()` の戻り値の型は `Promise
これが何を意味するか?
この関数を呼び出す側(コンポーネントやカスタムフック)で、以下のようなコードを書いたとする。
const UserProfile = () => {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUserData(‘123’).then(data => {
// TypeScriptは何も文句を言わない(型が `any` だから)
setUser(data.profile.naem); // ← タイポ(naem)してもコンパイルエラーにならない!
});
}, []);
return
;
};
`any` の汚染によって、TypeScriptの恩恵は完全に失われ、実行時エラー(`TypeError: Cannot read properties of undefined`)への特等席が用意される。
さらに恐ろしいのは、「意図せず狭い型に固定されるケース」だ。例えば、リテラル型やユニオン型を返すヘルパー関数でこれやると、後から拡張しようとした瞬間にコンパイルエラーの連鎖を引き起こす。
—
現場で即効性を持つ「正しい設計パターン」
では、プロダクションコードにおいて我々はどうアロー関数を書くべきか。答えはシンプルだ。
「関数の契約(Contract)である戻り値の型は、必ず明示せよ」
以下に、実務のフロントエンド開発(API連携・状態管理)を想定した、美しく保守性の高いコードを示す。コピペしてそのままチームの標準にして構わない。
プロダクションレディな実装例
import { useState, useEffect, useCallback } from ‘react’;
// ==========================================
// 1. ドメインモデルの定義(APIコントラクト)
// ==========================================
export interface UserProfile {
readonly id: string;
readonly name: string;
readonly email: string;
readonly role: ‘admin’ | ‘editor’ | ‘viewer’;
}
// APIエラーの型も明確に定義する
export interface ApiError {
readonly code: string;
readonly message: string;
}
// ==========================================
// 2. 戻り値の型を明示した堅牢なアロー関数
// ==========================================
/
- 指定されたユーザーのプロフィールを取得する
- @param userId ユーザーID
- @returns ユーザープロファイルのPromise。失敗時はApiErrorをスロー
/
export const fetchUserProfile = async (userId: string): Promise
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
const errorBody: ApiError = await response.json();
throw new Error(errorBody.message);
}
// ここで型アサーション (as UserProfile) を使うのではなく、
// 戻り値の型注釈により、TypeScriptがこのオブジェクトの構造を検証する
const data: UserProfile = await response.json();
return data;
};
// ==========================================
// 3. Reactコンポーネントでの実践的利用
// ==========================================
export const useUser = (userId: string) => {
const [user, setUser] = useState
const [loading, setLoading] = useState
const [error, setError } = useState
// 戻り値を明示しているため、IDEの補完が完全に効き、
// 呼び出し側でプロパティのタイポが起きる余地がない
const loadUser = useCallback(async (): Promise
setLoading(true);
setError(null);
try {
const data = await fetchUserProfile(userId);
setUser(data); // data は厳密に UserProfile型として扱われる
} catch (err) {
setError(err instanceof Error ? err.message : ‘Unknown error’);
} finally {
setLoading(false);
}
}, [userId]);
useEffect(() => {
loadUser();
}, [loadUser]);
return { user, loading, error, reload: loadUser };
};
—
テックリードからの設計指針・注意点
1. 「推論に頼るべき場所」と「明示すべき場所」の境界線
- 推論に任せるべき:
`const count = 10;` や、コールバック内の単純な変数など、スコープが極めて狭く、型が自明な場合。
- 必ず明示すべき:
公開API(他ファイルからインポートされる関数)、カスタムフック、コンポーネントのProps、そして「非同期アロー関数」の戻り値。 特に非同期処理は `Promise
2. パフォーマンスとコンパイル速度への影響
「型を明示するとコンパイルが遅くなるのでは?」という懸念を持つ者がいるが、それは誤解だ。
TypeScriptの型チェッカー(TSServer)にとって、人間が `): Promise
3. ESLintによる強制(自動化)
個人の意識に頼るようではアーキテクト失格だ。プロジェクト全体でこのルールを強制するためには、`eslint` の `@typescript-eslint/explicit-function-return-type` ルールを導入せよ。
{
“@typescript-eslint/explicit-function-return-type”: [
“error”,
{
“allowExpressions”: true,
“allowTypedFunctionExpressions”: true
}
]
}
これにより、戻り値の型がない関数はビルドエラーになり、チーム全体のコード品質が機械的に担保される。
—
まとめ
アロー関数の短さは美徳だが、それは「型安全」という土台があって初めて成り立つ。
型推論に甘えたコードは、書いた本人格好良く見えるかもしれないが、半年後のチームメンバー(あるいは未来の自分)に多大な負債を残す。
今日からコードを書くときは、アロー関数の右側に視線を移し、こう自問してほしい。
「この関数の契約(戻り値の型)は、コードの意図として明確に言語化されているか?」と。
妥協のないコードベースこそが、プロダクトをスケールさせる唯一の武器である。