はじめに
コードは他の開発者(未来の自分を含む)が読むものです。動作するコードを書くことは最低限の要求であり、真に価値のあるコードは「読みやすく、理解しやすく、変更しやすい」コードです。本記事では、『リーダブルコード』の原則をTypeScriptの文脈で実践的に解説します。
1. 名前付けの技術
明確で具体的な名前を選ぶ
// ❌ 悪い例
const d = new Date();
const yrs = calcAge(d);
// ✅ 良い例
const currentDate = new Date();
const userAgeInYears = calculateAge(currentDate);
検索しやすい名前を使う
// ❌ 悪い例
setTimeout(() => {
  processData();
}, 86400000); // 何の数字?
// ✅ 良い例
const MILLISECONDS_IN_A_DAY = 86400000;
setTimeout(() => {
  processData();
}, MILLISECONDS_IN_A_DAY);
境界値を明確にする
// ❌ 曖昧
interface Config {
  maxItems: number; // 含む?含まない?
}
// ✅ 明確
interface Config {
  maxItemsInclusive: number; // 最大値を含む
  // または
  itemLimit: number; // この値未満
}
2. 美しいコードフォーマット
一貫性のあるインデントとスペーシング
// ✅ 関連する要素をグループ化
class UserService {
  private readonly userRepository: UserRepository;
  private readonly emailService: EmailService;
  
  constructor(
    userRepository: UserRepository,
    emailService: EmailService
  ) {
    this.userRepository = userRepository;
    this.emailService = emailService;
  }
  
  async createUser(userData: CreateUserDto): Promise<User> {
    // バリデーション
    this.validateUserData(userData);
    
    // ユーザー作成
    const user = await this.userRepository.create(userData);
    
    // 通知送信
    await this.emailService.sendWelcomeEmail(user.email);
    
    return user;
  }
}
3. コメントの適切な使用
なぜ(Why)を説明する
// ❌ 何をしているかの説明(コードを読めばわかる)
// ユーザーIDをインクリメント
userId++;
// ✅ なぜそうするのかを説明
// レガシーシステムとの互換性のため、IDは1から始まる必要がある
userId++;
TODOコメントは具体的に
// ❌ 曖昧なTODO
// TODO: エラーハンドリングを改善
// ✅ 具体的なTODO
// TODO(tanaka): 2024-04-01までに、DBタイムアウトエラーに対するリトライ機能を実装
// 関連チケット: JIRA-1234
4. 制御フローの簡潔化
早期リターンで入れ子を減らす
// ❌ 深い入れ子
function processUser(user: User | null): string {
  if (user !== null) {
    if (user.isActive) {
      if (user.hasPermission) {
        return `Processing ${user.name}`;
      } else {
        return 'No permission';
      }
    } else {
      return 'User inactive';
    }
  } else {
    return 'User not found';
  }
}
// ✅ 早期リターン
function processUser(user: User | null): string {
  if (!user) return 'User not found';
  if (!user.isActive) return 'User inactive';
  if (!user.hasPermission) return 'No permission';
  
  return `Processing ${user.name}`;
}
条件式の順序
// ✅ 肯定的な条件を先に、単純な条件を先に
if (user.isActive && user.age >= 18) {
  // 通常の処理
} else {
  // エラー処理
}
5. 変数とスコープ
変数のスコープを最小限に
// ❌ 広すぎるスコープ
let total = 0;
let items: Item[] = [];
function calculateTotal() {
  items = fetchItems();
  total = items.reduce((sum, item) => sum + item.price, 0);
}
// ✅ 必要最小限のスコープ
function calculateTotal(): number {
  const items = fetchItems();
  const total = items.reduce((sum, item) => sum + item.price, 0);
  return total;
}
一時変数を削除
// ❌ 不要な一時変数
const isUserValid = user.age >= 18 && user.isVerified;
if (isUserValid) {
  // ...
}
// ✅ 直接使用(意味が明確な場合)
if (user.age >= 18 && user.isVerified) {
  // ...
}
// ✅ または説明的な関数として抽出
function isAdultAndVerified(user: User): boolean {
  return user.age >= 18 && user.isVerified;
}
6. コードの再構成
関数の抽出
// ❌ 長い関数
async function processOrder(order: Order): Promise<void> {
  // 在庫確認
  for (const item of order.items) {
    const stock = await getStock(item.productId);
    if (stock < item.quantity) {
      throw new Error(`Insufficient stock for ${item.productId}`);
    }
  }
  
  // 価格計算
  let total = 0;
  for (const item of order.items) {
    const price = await getPrice(item.productId);
    total += price * item.quantity;
  }
  
  // 注文処理
  // ... 長い処理
}
// ✅ 小さな関数に分割
async function processOrder(order: Order): Promise<void> {
  await validateStock(order.items);
  const total = await calculateTotal(order.items);
  await createOrder(order, total);
}
async function validateStock(items: OrderItem[]): Promise<void> {
  for (const item of items) {
    const stock = await getStock(item.productId);
    if (stock < item.quantity) {
      throw new InsufficientStockError(item.productId);
    }
  }
}
async function calculateTotal(items: OrderItem[]): Promise<number> {
  let total = 0;
  for (const item of items) {
    const price = await getPrice(item.productId);
    total += price * item.quantity;
  }
  return total;
}
重複の排除
// ❌ 重複したロジック
function validateEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}
function validateUserEmail(user: User): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(user.email);
}
// ✅ 共通化
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function isValidEmail(email: string): boolean {
  return EMAIL_REGEX.test(email);
}
function validateUserEmail(user: User): boolean {
  return isValidEmail(user.email);
}
7. テストを意識したコード
テストしやすい関数設計
// ❌ テストが困難
class OrderService {
  async processOrder(orderId: string): Promise<void> {
    const order = await database.getOrder(orderId);
    const user = await database.getUser(order.userId);
    
    // 直接メール送信
    await sendEmail(user.email, 'Order processed');
    
    // 直接データベース更新
    await database.updateOrder(orderId, { status: 'processed' });
  }
}
// ✅ 依存性注入でテスト可能に
class OrderService {
  constructor(
    private orderRepository: OrderRepository,
    private userRepository: UserRepository,
    private emailService: EmailService
  ) {}
  
  async processOrder(orderId: string): Promise<void> {
    const order = await this.orderRepository.findById(orderId);
    const user = await this.userRepository.findById(order.userId);
    
    await this.emailService.sendOrderConfirmation(user.email);
    await this.orderRepository.updateStatus(orderId, 'processed');
  }
}
8. エラーハンドリング
明確なエラーメッセージ
// ❌ 曖昧なエラー
throw new Error('Invalid input');
// ✅ 具体的なエラー
throw new ValidationError(
  `Invalid email format: "${email}". Expected format: user@example.com`
);
カスタムエラークラス
// ✅ ドメイン固有のエラー
class InsufficientStockError extends Error {
  constructor(
    public readonly productId: string,
    public readonly requested: number,
    public readonly available: number
  ) {
    super(
      `Insufficient stock for product ${productId}: ` +
      `requested ${requested}, available ${available}`
    );
    this.name = 'InsufficientStockError';
  }
}
// 使用例
if (stock < requested) {
  throw new InsufficientStockError(productId, requested, stock);
}
9. 巨大な式の分割
説明変数の導入
// ❌ 複雑な条件式
if (user.registrationDate > new Date('2023-01-01') && 
    user.purchases.length > 5 && 
    user.purchases.reduce((sum, p) => sum + p.amount, 0) > 1000) {
  // プレミアムユーザー処理
}
// ✅ 説明変数で意図を明確に
const isNewUser = user.registrationDate > new Date('2023-01-01');
const hasMultiplePurchases = user.purchases.length > 5;
const totalPurchaseAmount = user.purchases.reduce((sum, p) => sum + p.amount, 0);
const isHighValueCustomer = totalPurchaseAmount > 1000;
if (isNewUser && hasMultiplePurchases && isHighValueCustomer) {
  // プレミアムユーザー処理
}
ド・モルガンの法則を使った簡潔化
// ❌ 理解しにくい否定条件
if (!(isValid && isEnabled)) {
  return;
}
// ✅ ド・モルガンの法則で簡潔に
if (!isValid || !isEnabled) {
  return;
}
10. 型の活用
型エイリアスで意図を明確に
// ❌ プリミティブ型の直接使用
function calculateAge(birthYear: number): number {
  return new Date().getFullYear() - birthYear;
}
// ✅ 型エイリアスで意図を表現
type Year = number;
type Age = number;
function calculateAge(birthYear: Year): Age {
  return new Date().getFullYear() - birthYear;
}
ユニオン型で状態を表現
// ✅ 明確な状態管理
type LoadingState = {
  status: 'loading';
};
type SuccessState<T> = {
  status: 'success';
  data: T;
};
type ErrorState = {
  status: 'error';
  error: Error;
};
type DataState<T> = LoadingState | SuccessState<T> | ErrorState;
// 使用例
function renderUserProfile(state: DataState<User>) {
  switch (state.status) {
    case 'loading':
      return <Spinner />;
    case 'success':
      return <UserProfile user={state.data} />;
    case 'error':
      return <ErrorMessage error={state.error} />;
  }
}
11. 一度に一つのことを
タスクの分離
// ❌ 複数の責任を持つ関数
function updateUserAndSendNotification(userId: string, data: UpdateData) {
  // ユーザー検証
  const user = getUser(userId);
  if (!user) throw new Error('User not found');
  
  // データ更新
  user.name = data.name;
  user.email = data.email;
  user.updatedAt = new Date();
  
  // ログ記録
  console.log(`User ${userId} updated`);
  
  // メール送信
  sendEmail(user.email, 'Profile updated');
  
  // データベース保存
  saveUser(user);
}
// ✅ 単一責任の原則
function validateUser(userId: string): User {
  const user = getUser(userId);
  if (!user) throw new UserNotFoundError(userId);
  return user;
}
function updateUserData(user: User, data: UpdateData): User {
  return {
    ...user,
    ...data,
    updatedAt: new Date()
  };
}
function notifyUserUpdate(user: User): void {
  sendEmail(user.email, 'Profile updated');
}
// 使用時
async function handleUserUpdate(userId: string, data: UpdateData) {
  const user = validateUser(userId);
  const updatedUser = updateUserData(user, data);
  await saveUser(updatedUser);
  await notifyUserUpdate(updatedUser);
  logger.info(`User ${userId} updated`);
}
12. 短いコードより分かりやすいコード
可読性を優先
// ❌ 短いが理解しにくい
const a = u.filter(x => x.a && x.s > 18).map(x => x.n);
// ✅ 長いが理解しやすい
const activeAdultUsers = users
  .filter(user => user.isActive && user.age > 18)
  .map(user => user.name);
賢すぎるコードを避ける
// ❌ トリッキーなワンライナー
const result = (x => (y => x * y)(2))(3); // 6
// ✅ 単純明快
const multiplyBy2 = (x: number) => x * 2;
const result = multiplyBy2(3); // 6
13. コードに書かれていないことを読み取る
境界条件の明確化
// ❌ 境界条件が不明確
function getItems(count: number): Item[] {
  // countが0の場合は?負の場合は?
  return items.slice(0, count);
}
// ✅ 境界条件を明確に
function getItems(count: number): Item[] {
  if (count < 0) {
    throw new InvalidArgumentError('Count must be non-negative');
  }
  if (count === 0) {
    return [];
  }
  return items.slice(0, Math.min(count, items.length));
}
戻り値の意味を明確に
// ❌ nullの意味が不明確
function findUser(id: string): User | null {
  // nullは「見つからない」?「エラー」?
  return database.find(id);
}
// ✅ Result型で意図を明確に
type Result<T, E = Error> = 
  | { success: true; value: T }
  | { success: false; error: E };
function findUser(id: string): Result<User, UserError> {
  try {
    const user = database.find(id);
    if (!user) {
      return { 
        success: false, 
        error: new UserNotFoundError(id) 
      };
    }
    return { success: true, value: user };
  } catch (error) {
    return { 
      success: false, 
      error: new DatabaseError(error) 
    };
  }
}
14. ライブラリを知る
標準ライブラリの活用
// ❌ 車輪の再発明
function removeWhitespace(str: string): string {
  let result = '';
  for (const char of str) {
    if (char !== ' ' && char !== '\t' && char !== '\n') {
      result += char;
    }
  }
  return result;
}
// ✅ 標準メソッドを活用
function removeWhitespace(str: string): string {
  return str.replace(/\s/g, '');
}
配列メソッドの適切な使用
// ❌ forループでの実装
let hasExpensiveItem = false;
for (const item of items) {
  if (item.price > 1000) {
    hasExpensiveItem = true;
    break;
  }
}
// ✅ 適切な配列メソッド
const hasExpensiveItem = items.some(item => item.price > 1000);
// その他の便利なメソッド
const allItemsInStock = items.every(item => item.inStock);
const expensiveItems = items.filter(item => item.price > 1000);
const totalPrice = items.reduce((sum, item) => sum + item.price, 0);
15. 無関係の下位問題を抽出する
ユーティリティ関数の抽出
// ❌ ビジネスロジックに混在する汎用処理
function processOrder(order: Order) {
  // 日付フォーマット処理がビジネスロジックに混在
  const orderDate = new Date(order.createdAt);
  const formattedDate = `${orderDate.getFullYear()}-${
    String(orderDate.getMonth() + 1).padStart(2, '0')
  }-${String(orderDate.getDate()).padStart(2, '0')}`;
  
  console.log(`Processing order from ${formattedDate}`);
  // ... ビジネスロジック
}
// ✅ 汎用処理を分離
function formatDate(date: Date, format: string = 'YYYY-MM-DD'): string {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  
  return format
    .replace('YYYY', String(year))
    .replace('MM', month)
    .replace('DD', day);
}
function processOrder(order: Order) {
  const formattedDate = formatDate(new Date(order.createdAt));
  console.log(`Processing order from ${formattedDate}`);
  // ... ビジネスロジック
}
16. 汎用コードを特殊化する
ジェネリックの適切な使用
// ❌ 過度に汎用的
function processData<T, U, V>(
  data: T, 
  transformer: (t: T) => U, 
  validator: (u: U) => boolean,
  processor: (u: U) => V
): V | null {
  const transformed = transformer(data);
  if (validator(transformed)) {
    return processor(transformed);
  }
  return null;
}
// ✅ 用途に特化
interface OrderData {
  items: string[];
  total: number;
}
interface ValidatedOrder {
  items: OrderItem[];
  total: number;
  tax: number;
}
function processOrder(orderData: OrderData): ValidatedOrder | null {
  const validatedOrder = validateOrder(orderData);
  if (validatedOrder) {
    return calculateOrderTotals(validatedOrder);
  }
  return null;
}
まとめ
リーダブルなコードを書くことは、チーム開発において最も重要なスキルの一つです。以下の原則を常に意識しましょう:
- 名前は投資 - 良い名前を考える時間は、後で節約できる時間より遥かに少ない
 - シンプルに保つ - 複雑な解決策より、シンプルで理解しやすい解決策を選ぶ
 - 一貫性を保つ - チーム内でスタイルガイドを統一し、一貫性のあるコードを書く
 - リファクタリングを恐れない - コードは生き物。常に改善の余地がある
 - レビューを大切に - 他者の視点は、自分では気づかない問題を発見してくれる
 
これらの原則を実践することで、保守性が高く、バグの少ない、チームメンバー全員が理解できるコードベースを構築できるでしょう。