From 72af2ab3a1ef58e911ac9751ea0f720b8e16b6fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E6=9C=88?= Date: Sat, 25 Apr 2026 09:23:36 +0800 Subject: [PATCH] feat: add typescript-type-review skill --- hermes/typescript-type-review/SKILL.md | 290 +++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 hermes/typescript-type-review/SKILL.md diff --git a/hermes/typescript-type-review/SKILL.md b/hermes/typescript-type-review/SKILL.md new file mode 100644 index 0000000..5e8a4cf --- /dev/null +++ b/hermes/typescript-type-review/SKILL.md @@ -0,0 +1,290 @@ +--- +name: typescript-type-review +version: 1.0.0 +description: 审查 TypeScript 项目中的类型设计,找出类型安全漏洞、建模缺陷和可优化项,提出具体改进方案。适用于 code review、重构前分析、新包 API 设计评审。 +triggers: + - typescript type review + - type safety + - type audit + - TS 类型审查 + - 类型设计 + - type design + - discriminated union + - type narrowing + - Result type + - branded type +role: specialist +scope: review +output-format: structured +--- + +# TypeScript Type Review + +审查 TypeScript 项目中类型设计不够好的地方,提出具体优化方案。 + +## 触发条件 + +- 用户要求 review TypeScript 代码的类型设计 +- PR review 中涉及新的类型定义或 API surface +- 重构前评估类型健康度 +- 新包/模块的 API 设计评审 + +## 审查流程 + +### Step 1: 收集类型全貌 + +```bash +# 找所有类型定义 +search_files pattern="^export type " file_glob="*.ts" path="src/" + +# 找所有 interface(通常应该用 type) +search_files pattern="^export interface " file_glob="*.ts" path="src/" + +# 找 any 使用 +search_files pattern=": any[^_]|as any|" file_glob="*.ts" path="src/" + +# 找 optional properties(?:) +search_files pattern="\w+\?\s*:" file_glob="*.ts" path="src/" + +# 找类型断言 +search_files pattern="as [A-Z]\w+|<[A-Z]\w+>" file_glob="*.ts" path="src/" + +# 检查 tsconfig strict 设置 +read_file path="tsconfig.json" +``` + +### Step 2: 逐项审查 + +按以下检查清单逐项审查,每个问题给出 **文件:行号 + 当前代码 + 建议改法**。 + +## 审查清单 + +### 🔴 Critical — 类型安全漏洞 + +#### C1: `any` 逃逸 +`any` 会让类型检查完全失效,是最大的类型安全漏洞。 + +```typescript +// ❌ Bad +function parse(data: any): Config { ... } + +// ✅ Good — 用 unknown + 运行时校验 +function parse(data: unknown): Result { ... } +``` + +**例外**: 第三方库类型缺失时可用 `any`,但必须用 `// eslint-disable-next-line @typescript-eslint/no-explicit-any` 标注并加注释说明原因。 + +#### C2: 类型断言 (`as`) 绕过检查 +`as` 断言跳过编译器检查,隐藏潜在 bug。 + +```typescript +// ❌ Bad +const user = response.data as User; + +// ✅ Good — 运行时校验(zod / 手动 guard) +const parsed = userSchema.safeParse(response.data); +if (!parsed.success) return err(parsed.error); +``` + +**例外**: `as const` 是安全的;测试代码中构造 mock 数据时可酌情使用。 + +#### C3: 非空断言 (`!`) +`!` 告诉编译器"我保证不是 null",但运行时可能是。 + +```typescript +// ❌ Bad +const name = user.profile!.name; + +// ✅ Good +if (user.profile === null) return err({ kind: "no_profile" }); +const name = user.profile.name; +``` + +### ⚠️ Warning — 建模缺陷 + +#### W1: `optional (?)` vs `T | null` +Optional properties 让 `undefined` 和"未设置"语义混淆。显式 `T | null` 更清晰。 + +```typescript +// ❌ Ambiguous +type Config = { + timeout?: number; // 不设置?还是 undefined? +}; + +// ✅ Explicit +type Config = { + timeout: number | null; // null = 使用默认值 +}; +``` + +#### W2: 字符串枚举 vs 联合类型 +字符串枚举引入额外运行时对象,联合类型更轻量、类型推断更好。 + +```typescript +// ⚠️ 不推荐 +enum Status { Active = "active", Inactive = "inactive" } + +// ✅ 推荐 +type Status = "active" | "inactive"; +``` + +#### W3: 缺少 discriminated union +多个互斥状态用 optional fields 表示时,编译器无法帮你 narrow。 + +```typescript +// ❌ Bad — success 和 error 可以同时存在 +type Response = { + success: boolean; + data: User | null; + error: string | null; +}; + +// ✅ Good — discriminated union +type Response = + | { ok: true; value: User } + | { ok: false; error: string }; +``` + +#### W4: 函数签名用 throw 而非 Result +预期错误(网络失败、解析错误、校验失败)应返回 Result,不应 throw。 + +```typescript +// ❌ Bad — 调用方不知道会 throw 什么 +function fetchUser(id: string): Promise { ... } + +// ✅ Good — 错误类型在签名中可见 +type FetchError = { kind: "not_found" } | { kind: "network"; message: string }; +function fetchUser(id: string): Promise> { ... } +``` + +#### W5: 错误类型不够具体 +用 `Error` 或 `string` 表示错误丢失了结构化信息。 + +```typescript +// ❌ Bad +type Result = { ok: true; value: T } | { ok: false; error: string }; + +// ✅ Good — discriminated union 错误 +type SpawnError = + | { kind: "non_zero_exit"; exitCode: number; stderr: string } + | { kind: "timeout"; stdout: string; stderr: string } + | { kind: "spawn_failed"; message: string }; +``` + +#### W6: 宽泛类型参数 +`Record` 或 `object` 通常意味着缺少具体类型定义。 + +```typescript +// ❌ Bad +function configure(options: Record): void { ... } + +// ✅ Good +type ServerOptions = { + port: number; + host: string; + tls: TlsConfig | null; +}; +function configure(options: ServerOptions): void { ... } +``` + +### 💡 Suggestion — 可改可不改但能提升体验 + +#### S1: Branded Types 防止混淆 +当两个字段都是 `string` 但语义不同时,branded type 防止传错。 + +```typescript +type UserId = string & { readonly __brand: "UserId" }; +type TeamId = string & { readonly __brand: "TeamId" }; + +function getUser(id: UserId): User { ... } +// getUser(teamId) → 编译错误 ✅ +``` + +适用场景:ID、路径、token、URL 等容易混淆的 string 值。 + +#### S2: `Readonly` 和 `as const` +不可变数据用 `Readonly` 标注,防止意外修改。 + +```typescript +type Config = Readonly<{ + port: number; + routes: ReadonlyArray; +}>; +``` + +#### S3: 模板字面量类型 +当字符串有固定格式时,模板字面量提供编译期校验。 + +```typescript +type HexColor = `#${string}`; +type SemVer = `${number}.${number}.${number}`; +type EventName = `on${Capitalize}`; +``` + +#### S4: `satisfies` 替代 `as` +TS 4.9+ 的 `satisfies` 保留字面量类型的同时做类型检查。 + +```typescript +// ❌ as 丢失字面量类型 +const config = { port: 3000 } as Config; + +// ✅ satisfies 保留 { port: 3000 } +const config = { port: 3000 } satisfies Config; +``` + +#### S5: `infer` + 条件类型提取内部类型 +避免手动重复定义已经存在于其他类型中的子类型。 + +```typescript +// ❌ 手动重复 +type PromiseValue = User; // 和 fetchUser 返回类型耦合 + +// ✅ 自动提取 +type PromiseValue = T extends Promise ? U : T; +type User = PromiseValue>; +``` + +#### S6: 函数重载 vs 联合参数 +当不同参数组合对应不同返回类型时,用重载而非联合。 + +```typescript +// ❌ 返回类型不精确 +function parse(input: string | Buffer): string | Uint8Array; + +// ✅ 精确的输入→输出映射 +function parse(input: string): string; +function parse(input: Buffer): Uint8Array; +function parse(input: string | Buffer): string | Uint8Array { ... } +``` + +## 输出格式 + +审查结果按严重程度分组输出: + +```markdown +## TypeScript Type Review: [项目/包名] + +### 🔴 Critical (N) +- **src/api.ts:45** — C1: `any` 逃逸 + 当前: `function handle(req: any) { ... }` + 建议: 定义 `RequestPayload` 类型 + zod 校验 + +### ⚠️ Warning (N) +- **src/types.ts:12** — W3: 缺少 discriminated union + 当前: `type Result = { success: boolean; data?: T; error?: string }` + 建议: 改为 `{ ok: true; value: T } | { ok: false; error: E }` + +### 💡 Suggestion (N) +- **src/db.ts:8** — S1: userId 和 teamId 都是 string,可用 branded type 区分 + +### ✅ Good Patterns +- Result 使用一致 +- 类型导出粒度合理 +``` + +## 注意事项 + +- **尊重项目约定**: 先检查有没有 CLAUDE.md / CONTRIBUTING.md 等约定文件,以项目自身标准为准 +- **不要过度设计**: branded type、模板字面量等高级特性只在确实能防止真实 bug 时才建议 +- **测试代码放宽**: 测试中的 `as`、`any` 可以接受,但生产代码不行 +- **渐进式改进**: 对于大型项目,按 Critical → Warning → Suggestion 优先级分批修复,不要一次全改