9.5 KiB

name, version, description, triggers, role, scope, output-format
name version description triggers role scope output-format
typescript-type-review 1.0.0 审查 TypeScript 项目中的类型设计,找出类型安全漏洞、建模缺陷和可优化项,提出具体改进方案。适用于 code review、重构前分析、新包 API 设计评审。
typescript type review
type safety
type audit
TS 类型审查
类型设计
type design
discriminated union
type narrowing
Result type
branded type
specialist review structured

TypeScript Type Review

审查 TypeScript 项目中类型设计不够好的地方,提出具体优化方案。

触发条件

  • 用户要求 review TypeScript 代码的类型设计
  • PR review 中涉及新的类型定义或 API surface
  • 重构前评估类型健康度
  • 新包/模块的 API 设计评审

审查流程

Step 1: 收集类型全貌

# 找所有类型定义
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 会让类型检查完全失效,是最大的类型安全漏洞。

// ❌ 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。

// ❌ 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",但运行时可能是。

// ❌ 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 更清晰。

// ❌ Ambiguous
type Config = {
  timeout?: number;  // 不设置?还是 undefined?
};

// ✅ Explicit
type Config = {
  timeout: number | null;  // null = 使用默认值
};

W2: 字符串枚举 vs 联合类型

字符串枚举引入额外运行时对象,联合类型更轻量、类型推断更好。

// ⚠️ 不推荐
enum Status { Active = "active", Inactive = "inactive" }

// ✅ 推荐
type Status = "active" | "inactive";

W3: 缺少 discriminated union

多个互斥状态用 optional fields 表示时,编译器无法帮你 narrow。

// ❌ 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。

// ❌ 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: 错误类型不够具体

Errorstring 表示错误丢失了结构化信息。

// ❌ 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: Nullable 容器冗余 — Alternative 类型的 | null

当一个类型自身就有"空"表示(Haskell 中称为 Alternative typeclass 的 empty),再包一层 | null 就引入了两种"没有"的语义,调用方必须同时处理两个分支,且很难分清它们的区别。

常见的 Alternative 类型:T[](empty = [])、Record<string, T>(empty = {})、Map<K,V>Set<T>string(empty = "")。

核心问题:[]null 语义是否不同?如果相同,去掉 | null

// ❌ Bad — [] 和 null 都表示"没有标签",调用方要 if (tags === null || tags.length === 0)
type Post = {
  tags: string[] | null;
};

// ✅ Good — 空数组就是"没有"
type Post = {
  tags: string[];  // empty array = no tags
};
// ❌ Bad — {} 和 null 都表示"没有 headers"
type Request = {
  headers: Record<string, string> | null;
};

// ✅ Good
type Request = {
  headers: Record<string, string>;  // empty object = no headers
};

保留 | null 的合理场景:当 null 和 empty 确实有不同业务语义时。例如:

// ✅ 这里 null = "从未加载过",[] = "加载了但结果为空"
type SearchState = {
  results: SearchResult[] | null;  // null = not yet searched
};

审查方法:每次看到 T[] | nullRecord<K,V> | nullSet<T> | nullMap<K,V> | nullstring | null 时,问一句:empty 和 null 的语义是否不同? 如果答不上来,去掉 | null

W7: 宽泛类型参数

Record<string, unknown>object 通常意味着缺少具体类型定义。

// ❌ 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 防止传错。

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> 标注,防止意外修改。

type Config = Readonly<{
  port: number;
  routes: ReadonlyArray<Route>;
}>;

S3: 模板字面量类型

当字符串有固定格式时,模板字面量提供编译期校验。

type HexColor = `#${string}`;
type SemVer = `${number}.${number}.${number}`;
type EventName = `on${Capitalize<string>}`;

S4: satisfies 替代 as

TS 4.9+ 的 satisfies 保留字面量类型的同时做类型检查。

// ❌ as 丢失字面量类型
const config = { port: 3000 } as Config;

// ✅ satisfies 保留 { port: 3000 }
const config = { port: 3000 } satisfies Config;

S5: infer + 条件类型提取内部类型

避免手动重复定义已经存在于其他类型中的子类型。

// ❌ 手动重复
type PromiseValue = User; // 和 fetchUser 返回类型耦合

// ✅ 自动提取
type PromiseValue<T> = T extends Promise<infer U> ? U : T;
type User = PromiseValue<ReturnType<typeof fetchUser>>;

S6: 函数重载 vs 联合参数

当不同参数组合对应不同返回类型时,用重载而非联合。

// ❌ 返回类型不精确
function parse(input: string | Buffer): string | Uint8Array;

// ✅ 精确的输入→输出映射
function parse(input: string): string;
function parse(input: Buffer): Uint8Array;
function parse(input: string | Buffer): string | Uint8Array { ... }

输出格式

审查结果按严重程度分组输出:

## 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 时才建议
  • 测试代码放宽: 测试中的 asany 可以接受,但生产代码不行
  • 渐进式改进: 对于大型项目,按 Critical → Warning → Suggestion 优先级分批修复,不要一次全改