20
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

リーダブルコードの実践 - TypeScriptエンジニアのためのコーディング指針

Posted at

はじめに

コードは他の開発者(未来の自分を含む)が読むものです。動作するコードを書くことは最低限の要求であり、真に価値のあるコードは「読みやすく、理解しやすく、変更しやすい」コードです。本記事では、『リーダブルコード』の原則を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;
}

まとめ

リーダブルなコードを書くことは、チーム開発において最も重要なスキルの一つです。以下の原則を常に意識しましょう:

  1. 名前は投資 - 良い名前を考える時間は、後で節約できる時間より遥かに少ない
  2. シンプルに保つ - 複雑な解決策より、シンプルで理解しやすい解決策を選ぶ
  3. 一貫性を保つ - チーム内でスタイルガイドを統一し、一貫性のあるコードを書く
  4. リファクタリングを恐れない - コードは生き物。常に改善の余地がある
  5. レビューを大切に - 他者の視点は、自分では気づかない問題を発見してくれる

これらの原則を実践することで、保守性が高く、バグの少ない、チームメンバー全員が理解できるコードベースを構築できるでしょう。

20
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?