关于 typed-service-contracts
此 TypeScript 架构技能以契约和处理器定义服务,并采用解析后的输入及 Result 错误处理模式。适用于重视边界清晰、错误穷尽处理以及验证与业务逻辑分开测试的 CLI 和库开发。
主要功能
- 01Zod 输入与输出模式
- 02区分成功与失败的 Result
- 03处理器映射错误而不抛出异常
- 04契约测试与逻辑测试分离
适用场景
- 01为 CLI 定义严格的输入边界
- 02解析并转换复杂输入
- 03将模式测试与业务逻辑测试分开
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ google-labs-code/ design.md --skill typed-service-contracts安装后在 Claude Code 中输入/typed-service-contracts确认已加载。
详细介绍
类型化服务契约(Spec 与 Handler 模式)
本 Skill 定义了一种以契约式设计(Design by Contract,DbC)原则为基础的垂直切片架构(Vertical Slice Architecture)。它将应用逻辑视为严格定义的工作单元(Units of Work):输入需要被解析(而不仅仅是校验),错误则作为值处理(Result 模式),而不是以异常的形式抛出。
何时使用本 Skill
- 构建命令行工具或库: 当你需要在用户输入与系统逻辑之间建立严格边界时。
- 复杂校验: 当输入在被使用之前需要先转换(解析)时(例如,确保字符串是有效的文件路径)。
- 高可靠性要求: 当你无法承受未处理的运行时异常,并需要穷尽式的错误处理时。
- 聚焦测试: 当你希望将数据校验测试与业务逻辑测试分开时。
架构组件
1. Spec(spec.ts)
"契约"或"端口"。它定义了做什么(What)。它必须包含:
- 输入 Schema: 一个 Zod Schema,用于将原始输入解析为有效的 DTO。
- 输出 Schema: 一个 Zod Schema,定义成功时的数据结构。
- 错误 Schema: 具体失败模式的判别联合(discriminated union),而非通用错误。
- Result 类型:
Success | Failure的DiscriminatedUnion。 - 接口: 能力定义(例如
interface ConfigureSpec)。
2. Handler(handler.ts)
"实现"或"适配器"。它定义了如何做(How)。它必须:
- 实现 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(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 步:测试策略
不要编写单体式测试。将其拆分为契约测试(Contract Tests)与逻辑测试(Logic Tests)。
A. 契约测试(Schema)
测试守门人(Bouncer)。确保无效数据在到达 Handler 之前就被拒绝。
- 关注点: 边界情况、校验规则、Zod 精化(refinements)。
- 风格: 数据驱动(表格式测试)。
// 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. 逻辑测试(Handler)
测试厨师(Chef)。模拟外部依赖(fs、网络),并断言 Result 对象。
- 关注点: 业务逻辑流程、错误映射、成功状态。
- 风格: 模拟单元测试或场景运行器(Scenario Runners)。
// 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