typed-service-contracts について
入力のパースと Result によるエラー処理を取り入れ、契約とハンドラーでサービスを定義する TypeScript 向けアーキテクチャスキルです。境界の明確化や網羅的なエラー処理、検証とロジックの分離テストを重視する CLI・ライブラリ開発に適しています。
主な機能
- 01Zod による入出力スキーマ
- 02成功・失敗を判別する Result
- 03例外を投げずにエラーを変換するハンドラー
- 04契約テストとロジックテストの分離
活用シーン
- 01CLI の厳密な入力境界の定義
- 02複雑な入力のパースと変換
- 03スキーマとビジネスロジックの分離テスト
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ google-labs-code/ design.md --skill typed-service-contractsインストール後、Claude Code で/typed-service-contractsを入力して読み込まれたか確認します。
詳細
型付きサービス契約(仕様とハンドラーパターン)
このスキルでは、Design by Contract(DbC)の原則に基づく垂直スライスアーキテクチャを定義します。アプリケーションロジックを、厳密に定義された作業単位として扱います。入力は解析され(単に検証するだけではありません)、エラーは例外ではなく値(Result パターン)として扱われます。
このスキルを使用する場面
- CLI やライブラリの構築: ユーザー入力とシステムロジックの間に厳密な境界が必要な場合。
- 複雑な検証: 入力を利用可能な状態にする前に変換(解析)が必要な場合(例:文字列が有効なファイルパスであることを確認する)。
- 高い信頼性が求められる要件: 未処理のランタイム例外を許容できず、網羅的なエラー処理が必要な場合。
- テスト重視: データ検証テストとビジネスロジックテストを分離したい場合。
アーキテクチャの構成要素
1. 仕様(spec.ts)
「契約」または「ポート」です。何をするかを定義します。以下を含める必要があります。
- 入力スキーマ: 生の入力を有効な DTO に解析する Zod スキーマ。
- 出力スキーマ: 成功時のデータ構造を定義する Zod スキーマ。
- エラースキーマ: 特定の失敗モードを表す判別共用体(汎用エラーではありません)。
- 結果型:
Success | FailureのDiscriminatedUnion。 - インターフェース: 機能の定義(例:
interface ConfigureSpec)。
2. ハンドラー(handler.ts)
「実装」または「アダプター」です。どのように実行するかを定義します。以下を満たす必要があります。
- Spec で定義されたインターフェースを実装する。
- 副作用(ファイルシステム、API 呼び出し)を扱う「不純な」クラスである。
- 例外を絶対にスローしない。内部エラーをキャッチし、
Result型にマッピングする必要があります。
使用方法
ステップ 1:契約を定義する(spec.ts)
以下のテンプレートに従って境界を定義します。
import { z } from 'zod';
// 1. VALIDATION HELPERS (Reusable Refinements)
export const SafePathSchema = z.string()
.min(1)
.refine(p => !p.includes('..'), "No traversal allowed");
// 2. INPUT (The Command) - "Parse, don't validate"
export const MyTaskInputSchema = z.object({
path: SafePathSchema,
force: z.boolean().default(false),
});
export type MyTaskInput = z.infer<typeof MyTaskInputSchema>;
// 3. ERROR CODES (Exhaustive)
export const MyTaskErrorCode = z.enum([
'FILE_NOT_FOUND',
'PERMISSION_DENIED',
'UNKNOWN_ERROR'
]);
// 4. RESULT (The Monad)
export const MyTaskSuccess = z.object({
success: z.literal(true),
data: z.string(), // The output payload
});
export const MyTaskFailure = z.object({
success: z.literal(false),
error: z.object({
code: MyTaskErrorCode,
message: z.string(),
suggestion: z.string().optional(),
recoverable: z.boolean(),
})
});
export type MyTaskResult =
| z.infer<typeof MyTaskSuccess>
| z.infer<typeof MyTaskFailure>;
// 5. INTERFACE (The Capability)
export interface MyTaskSpec {
execute(input: MyTaskInput): Promise<MyTaskResult>;
}
ステップ 2:ハンドラーを実装する(handler.ts)
以下のテンプレートに従ってロジックを実装します。
import { MyTaskSpec, MyTaskInput, MyTaskResult } from './spec.js';
import * as fs from 'fs';
export class MyTaskHandler implements MyTaskSpec {
async execute(input: MyTaskInput): Promise<MyTaskResult> {
try {
// 1. Business Logic
if (!fs.existsSync(input.path)) {
// 2. Explicit Error Return (No Throwing)
return {
success: false,
error: {
code: 'FILE_NOT_FOUND',
message: `Path does not exist: ${input.path}`,
recoverable: true
}
};
}
// 3. Success Return
return {
success: true,
data: 'Operation complete'
};
} catch (error) {
// 4. Safety Net: Catch unknown runtime errors
return {
success: false,
error: {
code: 'UNKNOWN_ERROR',
message: error instanceof Error ? error.message : String(error),
recoverable: false
}
};
}
}
}
ステップ 3:テスト戦略
モノリシックなテストは書かないでください。契約テストとロジックテストに分けます。
A. 契約テスト(スキーマ)
門番をテストします。不正なデータがハンドラーに到達する前に拒否されることを確認します。
- 重点項目: エッジケース、検証ルール、Zod の精緻化。
- スタイル: データ駆動型(テーブルテスト)。
// spec.test.ts
import { MyTaskInputSchema } from './spec';
const invalidCases = [
{ val: '../etc/passwd', err: 'No traversal allowed' },
{ val: '', err: 'min(1)' },
];
test.each(invalidCases)('validates paths', ({ val, err }) => {
const result = MyTaskInputSchema.safeParse({ path: val });
expect(result.success).toBe(false);
});
B. ロジックテスト(ハンドラー)
シェフをテストします。外部依存関係(fs、ネットワーク)をモックし、Result オブジェクトを検証します。
- 重点項目: ビジネスロジックのフロー、エラーマッピング、成功状態。
- スタイル: モックを使用した単体テスト、またはシナリオランナー。
// handler.test.ts
import { MyTaskHandler } from './handler';
import { vi } from 'vitest'; // or jest
test('returns FILE_NOT_FOUND if path missing', async () => {
// MOCK
vi.mocked(fs.existsSync).mockReturnValue(false);
// EXECUTE
const handler = new MyTaskHandler();
const result = await handler.execute({ path: '/fake' });
// ASSERT (Check the Result Object)
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.code).toBe('FILE_NOT_FOUND');
}
});
コメント
Markdown 対応