条件分岐
条件分岐を記述する際のベストプラクティスを学びましょう。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;
}
改善ポイント:
- 早期リターンでネストを解消
- バリデーションロジックを分離
- エラーメッセージを構造化
条件分岐のまとめ
条件分岐のベストプラクティス
- 条件分岐は一箇所にまとめる
- 厳密等価演算子(
===)を使用する - 頻繁なケースから評価する
- 境界条件を変数化する
- 複雑な条件は関数に切り出す
- switch 文よりオブジェクトを検討
- 早期リターンでネストを減らす
- Discriminated Union で型安全に
- ポリモーフィズムで if 文を削減