はじめに
コードは他の開発者(未来の自分を含む)が読むものです。動作するコードを書くことは最低限の要求であり、真に価値のあるコードは「読みやすく、理解しやすく、変更しやすい」コードです。本記事では、『リーダブルコード』の原則を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;
}
まとめ
リーダブルなコードを書くことは、チーム開発において最も重要なスキルの一つです。以下の原則を常に意識しましょう:
- 名前は投資 - 良い名前を考える時間は、後で節約できる時間より遥かに少ない
- シンプルに保つ - 複雑な解決策より、シンプルで理解しやすい解決策を選ぶ
- 一貫性を保つ - チーム内でスタイルガイドを統一し、一貫性のあるコードを書く
- リファクタリングを恐れない - コードは生き物。常に改善の余地がある
- レビューを大切に - 他者の視点は、自分では気づかない問題を発見してくれる
これらの原則を実践することで、保守性が高く、バグの少ない、チームメンバー全員が理解できるコードベースを構築できるでしょう。