feat: add typescript-type-review skill
This commit is contained in:
parent
528774620b
commit
72af2ab3a1
290
hermes/typescript-type-review/SKILL.md
Normal file
290
hermes/typescript-type-review/SKILL.md
Normal file
@ -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|<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<Config, ParseError> { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
**例外**: 第三方库类型缺失时可用 `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<User> { ... }
|
||||||
|
|
||||||
|
// ✅ Good — 错误类型在签名中可见
|
||||||
|
type FetchError = { kind: "not_found" } | { kind: "network"; message: string };
|
||||||
|
function fetchUser(id: string): Promise<Result<User, FetchError>> { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### W5: 错误类型不够具体
|
||||||
|
用 `Error` 或 `string` 表示错误丢失了结构化信息。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Bad
|
||||||
|
type Result<T> = { 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<string, unknown>` 或 `object` 通常意味着缺少具体类型定义。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Bad
|
||||||
|
function configure(options: Record<string, unknown>): 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<T>` 和 `as const`
|
||||||
|
不可变数据用 `Readonly<T>` 标注,防止意外修改。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type Config = Readonly<{
|
||||||
|
port: number;
|
||||||
|
routes: ReadonlyArray<Route>;
|
||||||
|
}>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### S3: 模板字面量类型
|
||||||
|
当字符串有固定格式时,模板字面量提供编译期校验。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type HexColor = `#${string}`;
|
||||||
|
type SemVer = `${number}.${number}.${number}`;
|
||||||
|
type EventName = `on${Capitalize<string>}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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> = T extends Promise<infer U> ? U : T;
|
||||||
|
type User = PromiseValue<ReturnType<typeof fetchUser>>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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<T, E> 使用一致
|
||||||
|
- 类型导出粒度合理
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- **尊重项目约定**: 先检查有没有 CLAUDE.md / CONTRIBUTING.md 等约定文件,以项目自身标准为准
|
||||||
|
- **不要过度设计**: branded type、模板字面量等高级特性只在确实能防止真实 bug 时才建议
|
||||||
|
- **测试代码放宽**: 测试中的 `as`、`any` 可以接受,但生产代码不行
|
||||||
|
- **渐进式改进**: 对于大型项目,按 Critical → Warning → Suggestion 优先级分批修复,不要一次全改
|
||||||
Loading…
x
Reference in New Issue
Block a user