ゴール:「どこまでを1つの関数にして、どこから分けるか」を自分の基準で判断できるようになる
実務で一番モヤっとしやすいのがここです。
「この関数、長い気はするけど、どこで分ければいいの?」
「分けたら分けたで、逆に追いづらくならない?」
ここでは、“現場で本当に使える”関数分割ルールを、
TypeScript の関数・型設計と絡めながら、具体例付きで整理していきます。
「絶対こうしろ」という教科書ルールではなく、
あなたが自分で判断するときの“物差し”を増やすイメージで読んでください。
ルール1:「1つの関数は“1つの目的”だけにする」
目的が2つ以上混ざっていたら、分割候補
まず、一番シンプルで強力なルールです。
「この関数は、何をしている関数?」と聞かれて、
“そして”が出てきたら分割候補。
例を見てみます。
async function createUserAndSendMail(input: CreateUserInput): Promise<void> {
// 1. ユーザーをDBに保存
const user = await userRepository.save(input);
// 2. ログ出力
logger.info("user created", { userId: user.id });
// 3. メール送信
await mailer.sendWelcomeMail(user.email);
}
TypeScriptやっていることは明らかに3つあります。
- ユーザー作成
- ログ
- メール送信
これを分割すると、こうなります。
async function createUser(input: CreateUserInput): Promise<User> {
const user = await userRepository.save(input);
logger.info("user created", { userId: user.id });
return user;
}
async function sendWelcomeMailToUser(user: User): Promise<void> {
await mailer.sendWelcomeMail(user.email);
}
async function createUserAndSendMail(input: CreateUserInput): Promise<void> {
const user = await createUser(input);
await sendWelcomeMailToUser(user);
}
TypeScriptここでのポイントは、
- 「小さい関数」は“動詞+対象”で名前がつけやすい
- 大きい関数は「小さい関数を組み合わせる“シナリオ”」として読める
ようになっていることです。
「この関数、説明すると“〜して、〜して、〜する”って言っちゃうな」と思ったら、
その“そして”のところで分割できないか考えてみてください。
ルール2:「分岐ごとに“意味のある名前”が付けられるなら関数にする」
if / switch の中身が長くなってきたら要注意
例えば、こんなコードがあります。
function handleEvent(event: Event) {
if (event.type === "user_created") {
// ユーザー作成時の処理がズラズラ…
} else if (event.type === "user_deleted") {
// ユーザー削除時の処理がズラズラ…
} else if (event.type === "password_reset") {
// パスワードリセット時の処理がズラズラ…
}
}
TypeScriptこのまま育てていくと、handleEvent が「なんでも屋」になって破裂します。
ここで効くのが、
「この分岐の中身に、関数名をつけるとしたら?」
と自分に聞いてみることです。
function handleUserCreated(event: UserCreatedEvent) { /* ... */ }
function handleUserDeleted(event: UserDeletedEvent) { /* ... */ }
function handlePasswordReset(event: PasswordResetEvent) { /* ... */ }
TypeScriptこういう名前が自然に浮かぶなら、
それはもう関数にしてしまっていいサインです。
function handleEvent(event: Event) {
switch (event.type) {
case "user_created":
return handleUserCreated(event);
case "user_deleted":
return handleUserDeleted(event);
case "password_reset":
return handlePasswordReset(event);
}
}
TypeScriptTypeScript 的にも、
Eventを判別可能ユニオンにしておく- 各ハンドラの引数をそれぞれの型にする
と、型の絞り込みもきれいに効きます。
type UserCreatedEvent = { type: "user_created"; user: User };
type UserDeletedEvent = { type: "user_deleted"; userId: number };
type PasswordResetEvent = { type: "password_reset"; email: string };
type Event = UserCreatedEvent | UserDeletedEvent | PasswordResetEvent;
TypeScript分岐の中身が長くなってきたら、
「このブロックに“〜を処理する”という名前をつけられるか?」
を基準に、関数分割を検討してみてください。
ルール3:「データ変換」と「副作用」を分ける
変換ロジックは“純粋な関数”に閉じ込める
実務コードでよくあるのが、
「API レスポンスを変換して、そのまま画面に反映する」ような関数です。
async function loadAndRenderUser(id: number): Promise<void> {
const res = await api.get(`/users/${id}`);
const viewModel = {
id: res.data.id,
displayName: `${res.data.lastName} ${res.data.firstName}`,
email: res.data.email ?? "未登録",
};
renderUser(viewModel);
}
TypeScriptここには2種類のことが混ざっています。
- データ変換(APIレスポンス → ViewModel)
- 副作用(API呼び出し、画面描画)
変換部分だけを関数に切り出します。
type UserResponse = {
id: number;
firstName: string;
lastName: string;
email?: string;
};
type UserViewModel = {
id: number;
displayName: string;
email: string;
};
function toUserViewModel(res: UserResponse): UserViewModel {
return {
id: res.id,
displayName: `${res.lastName} ${res.firstName}`,
email: res.email ?? "未登録",
};
}
async function loadAndRenderUser(id: number): Promise<void> {
const res = await api.get<UserResponse>(`/users/${id}`);
const viewModel = toUserViewModel(res.data);
renderUser(viewModel);
}
TypeScriptこうすると、
toUserViewModelは「純粋な変換関数」としてテストしやすいloadAndRenderUserは「シナリオ(流れ)」だけを担当する
という役割分担になります。
関数の中に、
- データ変換
- ログ出力
- ネットワーク呼び出し
- DOM 操作
などが混ざり始めたら、
「変換だけをする小さい関数」に分けられないかを考えてみてください。
ルール4:「型が太り始めたら、関数を分けるサイン」
引数や戻り値の型が複雑になってきたら要注意
例えば、こんな関数があります。
function processUser(
user: {
id: number;
name: string;
email?: string;
roles: { id: number; name: string }[];
},
options?: {
sendMail?: boolean;
log?: boolean;
asAdmin?: boolean;
}
): {
success: boolean;
error?: string;
updatedUser?: {
id: number;
name: string;
email?: string;
roles: { id: number; name: string }[];
};
} {
// ...
}
TypeScript型がすでに悲鳴を上げています。
ここでやるべきことは2段階です。
1つ目は、型に名前をつけて外に出すこと。
type User = {
id: number;
name: string;
email?: string;
roles: { id: number; name: string }[];
};
type ProcessUserOptions = {
sendMail?: boolean;
log?: boolean;
asAdmin?: boolean;
};
type ProcessUserResult =
| { success: true; updatedUser: User }
| { success: false; error: string };
TypeScript2つ目は、「この関数、責務を分けられないか?」を考えることです。
例えば、
- ユーザーの更新ロジック
- メール送信
- ログ出力
を分けてしまう。
function updateUser(user: User, options: ProcessUserOptions): ProcessUserResult {
// ユーザーの更新だけ
}
function maybeSendMail(user: User, options: ProcessUserOptions): void {
// sendMail オプションを見てメール送信
}
function maybeLog(user: User, options: ProcessUserOptions): void {
// log オプションを見てログ出力
}
function processUser(user: User, options: ProcessUserOptions): ProcessUserResult {
const result = updateUser(user, options);
if (!result.success) return result;
maybeSendMail(result.updatedUser, options);
maybeLog(result.updatedUser, options);
return result;
}
TypeScript「型が太ってきた」というのは、
「この関数に情報と責務を詰め込みすぎている」サインです。
型を整理しながら、
「この関数、本当は何個の役割を持っている?」と問い直してみてください。
ルール5:「テストを書こうとしたときに“つらい”なら分ける」
テストしづらさは、関数分割の強いシグナル
例えば、こんな関数をテストしようとします。
async function registerUser(input: RegisterInput): Promise<void> {
const user = await userRepository.save(input);
await mailer.sendWelcomeMail(user.email);
logger.info("user registered", { userId: user.id });
}
TypeScriptこれをテストしようとすると、
- DB をモックして…
- メール送信をモックして…
- ロガーもモックして…
と、準備が大変になります。
ここでの違和感は、
「1つの関数をテストするために、モックしなきゃいけないものが多すぎる」
というところにあります。
関数を分けてみます。
async function saveUser(input: RegisterInput): Promise<User> {
const user = await userRepository.save(input);
logger.info("user registered", { userId: user.id });
return user;
}
async function sendWelcomeMail(user: User): Promise<void> {
await mailer.sendWelcomeMail(user.email);
}
async function registerUser(input: RegisterInput): Promise<void> {
const user = await saveUser(input);
await sendWelcomeMail(user);
}
TypeScriptこれなら、
saveUserのテストでは「DB とログ」だけを意識すればいいsendWelcomeMailのテストでは「メール送信」だけを意識すればいいregisterUserは「2つの関数が呼ばれる順番」をテストすればいい
というふうに、テストの焦点が分かれます。
「この関数、テストを書こうとしたときに、モックが3つ以上必要になる」
と感じたら、それは分割を検討する強いサインです。
まとめ:実務での関数分割ルールを自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
関数を分割するかどうか迷ったら、次を順番に自問する。
- この関数を説明するとき、「〜して、〜して、〜する」と“そして”が出てこないか
- if / switch の各ブロックに「〜を処理する」という名前をつけられないか
- データ変換と副作用が混ざっていないか(変換だけの関数にできないか)
- 引数・戻り値の型が太りすぎていないか(型を整理したら責務の多さが見えないか)
- テストを書こうとしたとき、モックが多すぎてつらくないか
どれか1つでも「うん、そうかも」と思ったら、
その関数は“分割候補”です。
大事なのは、「短い関数を量産すること」ではなく、
「1つの関数に“1つの役割”だけを持たせる」
という感覚を、少しずつ身体に染み込ませていくことです。
今書いている関数を1つ選んで、
上の5つを当てはめてみませんか。
どこを切り出せそうか、一緒に言語化してみてもいいですよ。

