メインコンテンツまでスキップ

条件分岐

条件分岐を記述する際のベストプラクティスを学びましょう。TypeScriptの型システムを活用することで、より安全で読みやすい条件分岐を書くことができます。

学習のポイント

  • 条件式を伝わりやすくする
  • 読み手の負担を減らす
  • 早期リターンを活用する
  • ネストを深くしない
  • if 文よりも他の手法を検討する

条件分岐はあちこちで書かない

同じ条件分岐が複数箇所に散らばっていると、仕様変更時の影響範囲が広がります。条件分岐は一箇所にまとめましょう。

Bad

❌ Bad: 同じ条件分岐が散在
// ❌ 同じ条件分岐が散在
type SkillName = 'たいあたり' | 'でんこうせっか' | 'アイアンテール' | '10まんボルト' | 'かみなり';

function getAttackPoint(skill: SkillName): number {
switch (skill) {
case 'アイアンテール':
return 30;
case '10まんボルト':
return 100;
case 'かみなり':
return 1000;
default:
return 0;
}
}

function getHp(skill: SkillName, currentHp: number): number {
let attackPoint: number;
if (skill === 'たいあたり') {
attackPoint = 10;
} else if (skill === 'でんこうせっか') {
attackPoint = 20;
} else {
attackPoint = getAttackPoint(skill);
}
return currentHp - attackPoint;
}

Good

✅ Good: 条件分岐を一箇所にまとめる
// ✅ 条件分岐を一箇所にまとめる
type SkillName = 'たいあたり' | 'でんこうせっか' | 'アイアンテール' | '10まんボルト' | 'かみなり';

const SKILL_ATTACK_POINTS: Record<SkillName, number> = {
'たいあたり': 10,
'でんこうせっか': 20,
'アイアンテール': 30,
'10まんボルト': 100,
'かみなり': 1000,
} as const;

function getAttackPoint(skill: SkillName): number {
return SKILL_ATTACK_POINTS[skill];
}

function getHp(skill: SkillName, currentHp: number): number {
const attackPoint = getAttackPoint(skill);
return currentHp - attackPoint;
}

厳密な等価性で評価する

TypeScriptでも ===(厳密等価演算子)を使用しましょう。==(等価演算子)は暗黙的な型変換が行われるため、予期しないバグの原因になります。

Bad

❌ Bad: 等価演算子は型変換が発生
// ❌ 等価演算子は型変換が発生
if ('12' == 10 + 2) { } // true(文字列が数値に変換される)
if (0 == false) { } // true(falseが0に変換される)
if (null == undefined) { } // true(nullとundefinedは等価)

Good

✅ Good: 厳密等価演算子を使用
// ✅ 厳密等価演算子を使用
if ('12' === String(10 + 2)) { } // 明示的に型を合わせる
if (0 === 0) { } // 同じ型で比較
if (value === null || value === undefined) { } // 明示的にチェック

// ✅ nullish チェックには ?? や ?. を使用
const result = value ?? 'default';
const name = user?.profile?.name;

最も一般的な条件から評価する

if文の条件式は最も頻繁に発生するケースから記述しましょう。

Bad

❌ Bad: まれなケースから評価
// ❌ まれなケースから評価
type UserType = 'free' | 'premium' | 'admin';

function processUser(user: User): void {
if (user.type === 'admin') {
// 管理者アカウントの処理(最も少ない)
} else if (user.type === 'premium') {
// プレミアムアカウントの処理
} else {
// 通常アカウントの処理(最も多い)
}
}

Good

✅ Good: 頻繁なケースから評価
// ✅ 頻繁なケースから評価
function processUser(user: User): void {
if (user.type === 'free') {
// 通常アカウントの処理(最も多い)
} else if (user.type === 'premium') {
// プレミアムアカウントの処理
} else {
// 管理者アカウントの処理(最も少ない)
}
}

境界条件のカプセル化

境界条件に使用している値を適切に変数で置換することで、コードの可読性が向上します。

Bad

❌ Bad: マジックナンバーが散在
// ❌ マジックナンバーが散在
if (age >= 18 && age < 65) {
console.log('労働年齢人口です');
}

if (items.length > 100) {
console.log('ページネーションが必要です');
}

Good

✅ Good: 境界条件を変数化
// ✅ 境界条件を変数化
const WORKING_AGE_MIN = 18;
const WORKING_AGE_MAX = 65;
const MAX_ITEMS_PER_PAGE = 100;

const isWorkingAge = age >= WORKING_AGE_MIN && age < WORKING_AGE_MAX;
if (isWorkingAge) {
console.log('労働年齢人口です');
}

const needsPagination = items.length > MAX_ITEMS_PER_PAGE;
if (needsPagination) {
console.log('ページネーションが必要です');
}

複雑な条件式は関数に切り出す

複雑な条件式は関数にまとめることで、可読性が向上し、再利用も可能になります。

Bad

❌ Bad: 条件式が複雑で読みづらい
// ❌ 条件式が複雑で読みづらい
if (user.age >= 18 && user.isEmailVerified && !user.isBanned && user.subscription.status === 'active') {
// 処理
}

// ❌ 正規表現が条件式に直接書かれている
const regex = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
if (regex.test(text)) {
console.log('絵文字が含まれています');
}

Good

✅ Good: 条件を関数として抽出
// ✅ 条件を関数として抽出
function canAccessPremiumContent(user: User): boolean {
return (
user.age >= 18 &&
user.isEmailVerified &&
!user.isBanned &&
user.subscription.status === 'active'
);
}

if (canAccessPremiumContent(user)) {
// 処理
}

// ✅ 正規表現も関数化
function containsEmoji(text: string): boolean {
const emojiRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
return emojiRegex.test(text);
}

if (containsEmoji(text)) {
console.log('絵文字が含まれています');
}

switch 文よりも連想配列(オブジェクト)を利用する

switch文は冗長になりがちです。TypeScriptのRecord型を使った連想配列で置き換えることを検討しましょう。

Bad

❌ Bad: switch文が長くなる
// ❌ switch文が長くなる
function getAreaName(areaCode: number): string {
switch (areaCode) {
case 1:
return '千代田区';
case 2:
return '中央区';
case 3:
return '港区';
case 4:
return '新宿区';
default:
return '他の区';
}
}

Good

✅ Good: オブジェクトで管理
// ✅ オブジェクトで管理
const AREA_NAMES: Record<number, string> = {
1: '千代田区',
2: '中央区',
3: '港区',
4: '新宿区',
};

function getAreaName(areaCode: number): string {
return AREA_NAMES[areaCode] ?? '他の区';
}

// ✅ 型安全にする場合
type AreaCode = 1 | 2 | 3 | 4;

const AREA_NAMES_SAFE: Record<AreaCode, string> = {
1: '千代田区',
2: '中央区',
3: '港区',
4: '新宿区',
};

function getAreaNameSafe(areaCode: AreaCode): string {
return AREA_NAMES_SAFE[areaCode];
}

早期リターンを使う

早期リターン(Guards節)を使うことで、ネストを減らし可読性を向上させます。

Bad

❌ Bad: ネストが深い
// ❌ ネストが深い
async function getUserById(userId: number): Promise<User | null> {
if (typeof userId === 'number' && userId > 0) {
const response = await fetch('/users', {
method: 'POST',
body: JSON.stringify({ userId }),
});
if (response.ok) {
const result = await response.json();
return result;
}
return null;
}
return null;
}

Good

✅ Good: 早期リターンでフラットに
// ✅ 早期リターンでフラットに
async function getUserById(userId: number): Promise<User | null> {
if (typeof userId !== 'number' || userId <= 0) {
return null;
}

const response = await fetch('/users', {
method: 'POST',
body: JSON.stringify({ userId }),
});

if (!response.ok) {
return null;
}

return response.json();
}

ネストを防ぐ

ネストが深いコードは読みづらく、バグの原因になります。

Bad

❌ Bad: ネストが深い
// ❌ ネストが深い
function isActiveUser(user: User | null): boolean {
if (user != null) {
if (user.startDate <= today && (user.endDate == null || today <= user.endDate)) {
if (user.stopped) {
return false;
} else {
return true;
}
} else {
return false;
}
} else {
return false;
}
}

Good

✅ Good: 早期リターンでフラットに
// ✅ 早期リターンでフラットに
function isActiveUser(user: User | null): boolean {
if (user === null) return false;
if (user.startDate > today) return false;
if (user.endDate !== null && today > user.endDate) return false;
if (user.stopped) return false;

return true;
}

// ✅ さらに条件を関数化
function isWithinActivePeriod(user: User, today: Date): boolean {
return user.startDate <= today && (user.endDate === null || today <= user.endDate);
}

function isActiveUser(user: User | null): boolean {
if (user === null) return false;
if (!isWithinActivePeriod(user, today)) return false;
if (user.stopped) return false;

return true;
}

TypeScriptの型による条件分岐

TypeScriptでは、型を使って条件分岐を安全に行うことができます。

Discriminated Union(判別可能なユニオン型)

✅ Good: type プロパティで判別
// ✅ type プロパティで判別
type Shape =
| { type: 'circle'; radius: number }
| { type: 'rectangle'; width: number; height: number }
| { type: 'triangle'; base: number; height: number };

function calculateArea(shape: Shape): number {
switch (shape.type) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
}
}

exhaustiveness check(網羅性チェック)

✅ Good: 全てのケースを網羅していることをコンパイラが確認
// ✅ 全てのケースを網羅していることをコンパイラが確認
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}

type Status = 'pending' | 'approved' | 'rejected';

function getStatusMessage(status: Status): string {
switch (status) {
case 'pending':
return '承認待ちです';
case 'approved':
return '承認されました';
case 'rejected':
return '却下されました';
default:
return assertNever(status); // 新しいStatusが追加されたらコンパイルエラー
}
}

コールバック関数やクラスで if 文を削減

if文を減らすことで、コードの複雑さを下げることができます。

Bad

❌ Bad: if文で処理を分岐
// ❌ if文で処理を分岐
function executeQuery(type: 'username' | 'password', data: UserData): void {
const db = getDBConnection();
db.startTransaction();

if (type === 'username') {
updateUsername(data, db);
} else if (type === 'password') {
updatePassword(data, db);
}

db.endTransaction();
}

Good

✅ Good: コールバック関数で処理を注入
// ✅ コールバック関数で処理を注入
type UpdateFunction = (data: UserData, db: Database) => void;

function executeQuery(updateFn: UpdateFunction, data: UserData): void {
const db = getDBConnection();
db.startTransaction();

updateFn(data, db);

db.endTransaction();
}

// 使用時
executeQuery(updateUsername, { userId: 1, userName: 'Tom' });
executeQuery(updatePassword, { userId: 1, password: 'secret' });

ポリモーフィズムで if 文を削除

✅ Good: インターフェースとクラスで分岐を排除
// ✅ インターフェースとクラスで分岐を排除
interface Animal {
eat(): void;
sleep(): void;
}

class Dog implements Animal {
eat(): void {
console.log('ドッグフードを食べる');
}
sleep(): void {
console.log('小屋で寝る');
}
}

class Cat implements Animal {
eat(): void {
console.log('キャットフードを食べる');
}
sleep(): void {
console.log('ソファーで寝る');
}
}

function actAnimal(animal: Animal): void {
animal.eat();
animal.sleep();
}

// 使用時(if文なし)
const dog = new Dog();
actAnimal(dog);

const cat = new Cat();
actAnimal(cat);

練習問題

以下のコードを早期リターンでリファクタリングしてください
// 問題
function isCorrectInput(member: Member): boolean {
if (member.id) {
if (member.name) {
if (member.age) {
if (member.email) {
return true;
} else {
console.error('メールアドレスが入力されていません');
return false;
}
} else {
console.error('年齢が入力されていません');
return false;
}
} else {
console.error('名前が入力されていません');
return false;
}
} else {
console.error('idがありません');
return false;
}
}

解答:

interface ValidationResult {
isValid: boolean;
error?: string;
}

function validateMember(member: Member): ValidationResult {
if (!member.id) {
return { isValid: false, error: 'idがありません' };
}
if (!member.name) {
return { isValid: false, error: '名前が入力されていません' };
}
if (!member.age) {
return { isValid: false, error: '年齢が入力されていません' };
}
if (!member.email) {
return { isValid: false, error: 'メールアドレスが入力されていません' };
}
return { isValid: true };
}

function isCorrectInput(member: Member): boolean {
const result = validateMember(member);
if (!result.isValid && result.error) {
console.error(result.error);
}
return result.isValid;
}

改善ポイント:

  1. 早期リターンでネストを解消
  2. バリデーションロジックを分離
  3. エラーメッセージを構造化

条件分岐のまとめ

条件分岐のベストプラクティス
  • 条件分岐は一箇所にまとめる
  • 厳密等価演算子(===)を使用する
  • 頻繁なケースから評価する
  • 境界条件を変数化する
  • 複雑な条件は関数に切り出す
  • switch 文よりオブジェクトを検討
  • 早期リターンでネストを減らす
  • Discriminated Union で型安全に
  • ポリモーフィズムで if 文を削減