Robustness Rule
このドキュメントは、コードの堅牢性を確保するための設計原則とコーディング規約を定義します。
堅牢性設計ガイドライン
このドキュメントは、コードの堅牢性を確保するための設計原則とコーディング規約を定義します。
基本理念
Design for Correctness(正しさのための設計)
テストで正しさを検証するのではなく、設計によって正しさを保証する。
| アプローチ | 説明 | 優先度 | |-----------|------|--------| | 型による保証 | コンパイル時に不正な状態を検出 | 最優先 | | 静的解析 | linterやTypeScriptの厳格な設定で問題を早期発見 | 高 | | ランタイム検証 | Zodによる境界でのバリデーション | 中 | | テスト | 上記で保証できない振る舞いの検証 | 補完的 |
「動かないコードは書けない」設計 > 「動かないコードを見つける」テスト
シンプルさと堅牢性の両立
堅牢性のために複雑さを導入するのではなく、シンプルな設計で堅牢性を達成することを目指す。
- 不要な抽象化を避ける
- 明示的で予測可能なコードを書く
- 型システムを活用して暗黙の契約を明示する
型安全性ツールの活用
1. ts-pattern(パターンマッチング)
使用目的: 条件分岐の型安全性とexhaustive checking
import { match, P } from 'ts-pattern';
// ✅ Good: exhaustive checkingによる網羅性保証
type Status = 'pending' | 'running' | 'completed' | 'failed';
const getStatusMessage = (status: Status): string =>
match(status)
.with('pending', () => '待機中')
.with('running', () => '実行中')
.with('completed', () => '完了')
.with('failed', () => '失敗')
.exhaustive(); // 新しいstatusが追加されたらコンパイルエラー
// ✅ Good: 複雑なオブジェクトのパターンマッチ
match(result)
.with({ isOk: true }, ({ value }) => handleSuccess(value))
.with({ isErr: true, error: { type: 'NOT_FOUND' } }, () => handleNotFound())
.with({ isErr: true, error: { type: 'TIMEOUT' } }, () => handleTimeout())
.otherwise(() => handleUnexpected());
使用すべき場面:
- Union型の分岐処理
- エラーハンドリング
- 状態遷移の処理
- 複数条件の組み合わせ判定
例外(通常のif文でよい場合):
- 単純なboolean判定(
if (isLoading)) - null/undefinedチェックのみ
2. Zod(スキーマバリデーション)
使用目的: 外部境界でのランタイム検証と型推論
import { z } from 'zod';
// ✅ Good: スキーマから型を導出
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['admin', 'user', 'guest']),
});
type User = z.infer<typeof UserSchema>;
// ✅ Good: 外部入力の検証
const parseUser = (input: unknown): User => {
return UserSchema.parse(input); // 失敗時はZodError
};
// ✅ Good: 安全なパース(Result型との組み合わせ)
const safeParseUser = (input: unknown): Result<User, ValidationError> => {
const result = UserSchema.safeParse(input);
if (result.success) {
return ok(result.data);
}
return err({ type: 'VALIDATION_ERROR', issues: result.error.issues });
};
使用すべき場面:
- API境界(tRPCのinput/output)
- ファイル読み込み後のデータ検証
- 設定ファイルのパース
- ユーザー入力の検証
3. Zod Branded Types(公称型)
使用目的: プリミティブ型の意味的な区別
// ✅ Good: Branded Typeで意味を明確化
const UserIdSchema = z.string().uuid().brand<'UserId'>();
const PhotoIdSchema = z.string().uuid().brand<'PhotoId'>();
type UserId = z.infer<typeof UserIdSchema>;
type PhotoId = z.infer<typeof PhotoIdSchema>;
// コンパイル時に混同を防止
function getPhoto(photoId: PhotoId): Photo { ... }
const userId: UserId = UserIdSchema.parse('...');
const photoId: PhotoId = PhotoIdSchema.parse('...');
getPhoto(photoId); // ✅ OK
getPhoto(userId); // ❌ コンパイルエラー!
使用すべき場面:
- ID型(UserId, PhotoId, WorldId など)
- パス型(絶対パス、相対パス)
- 検証済みの値(ValidatedEmail, NormalizedPath など)
4. BaseValueObject パターン
プロジェクト固有のValueObjectパターンを使用する場合:
// 型のみをエクスポート
class MyValueObject extends BaseValueObject<'MyValueObject', string> {}
export type { MyValueObject }; // ✅ 型のみ
// Zodスキーマ経由でインスタンス生成
export const MyValueObjectSchema = z.string().transform(
(val) => new MyValueObject(val)
);
// 使用側
const obj = MyValueObjectSchema.parse(value); // ✅ 正しい
const obj = new MyValueObject(value); // ❌ 直接newは禁止
静的解析の活用
TypeScript設定
tsconfig.json で厳格な設定を使用:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true
}
}
カスタムlinterの活用
| コマンド | 目的 |
|---------|------|
| pnpm lint:neverthrow | Result型の正しい使用を検証 |
| pnpm lint:valueobjects | ValueObjectパターンの遵守を検証 |
| pnpm lint:ts-pattern | ts-patternの適切な使用を検証 |
設計パターン
不正な状態を表現不可能にする
// ❌ Bad: 不正な状態が表現可能
interface LoadingState {
isLoading: boolean;
data: Data | null;
error: Error | null;
}
// isLoading=true かつ data!=null かつ error!=null という状態が可能
// ✅ Good: 不正な状態が型レベルで表現不可能
type LoadingState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: Data }
| { status: 'error'; error: Error };
Parse, Don't Validate
// ❌ Bad: 検証後も型が変わらない
function processEmail(email: string): void {
if (!isValidEmail(email)) {
throw new Error('Invalid email');
}
// email は依然として string 型
sendEmail(email);
}
// ✅ Good: 検証と型変換を同時に行う
const EmailSchema = z.string().email().brand<'Email'>();
type Email = z.infer<typeof EmailSchema>;
function processEmail(input: string): Result<void, ValidationError> {
const emailResult = EmailSchema.safeParse(input);
if (!emailResult.success) {
return err({ type: 'VALIDATION_ERROR' });
}
// emailResult.data は Email 型(検証済みであることが型で保証)
return sendEmail(emailResult.data);
}
早期リターンと型の絞り込み
// ✅ Good: 早期リターンで型を絞り込む
function processUser(user: User | null): Result<ProcessedUser, Error> {
if (!user) {
return err({ type: 'USER_NOT_FOUND' });
}
// ここ以降、user は User 型(nullでないことが保証)
if (!user.isActive) {
return err({ type: 'USER_INACTIVE' });
}
// ここ以降、user.isActive は true(アクティブであることが保証)
return ok(processActiveUser(user));
}
優先順位の判断基準
堅牢性とシンプルさがトレードオフになる場合の判断基準:
| 状況 | 推奨アプローチ | |------|---------------| | 外部入力(API、ファイル) | 堅牢性優先(Zod必須) | | 内部のドメインロジック | 型による保証 + ts-pattern | | ID型の混同リスク | Branded Types | | 状態遷移 | Union型 + exhaustive matching | | 単純なユーティリティ | シンプルさ優先 |
アンチパターン
1. any/unknownの安易な使用
// ❌ Bad
function process(data: any): void { ... }
// ✅ Good
function process(data: unknown): Result<ProcessedData, ParseError> {
const parsed = DataSchema.safeParse(data);
...
}
2. 型アサーション(as)の濫用
// ❌ Bad
const user = response.data as User;
// ✅ Good
const userResult = UserSchema.safeParse(response.data);
3. オプショナルチェーンの過剰使用
// ❌ Bad: nullableが伝播して型が曖昧に
const name = user?.profile?.settings?.displayName ?? 'Unknown';
// ✅ Good: 明示的なnullチェックと早期リターン
if (!user) return err({ type: 'USER_NOT_FOUND' });
if (!user.profile) return err({ type: 'PROFILE_NOT_FOUND' });
return ok(user.profile.settings.displayName);
関連ドキュメント
.claude/rules/error-handling.md- エラーハンドリングの詳細docs/lint-neverthrow.md- neverthrowリンターの使用方法