Claude Code 与 TypeScript:更快也更安全的实战技巧
用 strict、Zod、Union、泛型、satisfies 和类型测试提升 Claude Code 的 TypeScript 质量。
Claude Code 能明显加快 TypeScript 开发,尤其是表单、API helper 和测试代码。 风险也很直接:如果类型边界不清楚,它会同样快速地生成脆弱代码。 对初学者来说,最稳的习惯是先写清规则,再让 Claude Code 写功能。
这里的 strict 可以理解为“让 TypeScript 不放过可疑代码”的设置;领域类型是“把业务规则写成类型”;判别式 Union 是“用状态字段区分不同数据形状”;运行时校验则是“程序运行时检查值是否真的符合规则”。
目标不是炫技,而是让 AI 生成的 TypeScript 更容易审查、更少踩坑。
先给 Claude Code 一张类型地图
不要一开始就只写“帮我实现这个功能”。 先把编译规则、领域类型、外部输入、状态和类型测试告诉它。
flowchart TD
A["需求"] --> B["tsconfig: strict 规则"]
B --> C["领域类型: Plan 和 Account"]
C --> D["外部数据: unknown 后校验"]
D --> E["状态: 判别式 Union"]
E --> F["类型测试: expectTypeOf / tsd"]
F --> G["Claude Code 实现和复查"]
官方资料建议看 strict、noUncheckedIndexedAccess、exactOptionalPropertyTypes、Narrowing、Generics、Utility Types 和 satisfies 说明。
运行时校验可以同时参考 Zod 文档。
相关基础可以继续看 TypeScript Utility Types、TypeScript Generics 和 Zod Validation。
从 strict tsconfig 开始
只说“用 TypeScript 写”还不够。 先固定编译器规则,Claude Code 的输出会稳定很多。
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"]
}
提示词里也要写清约束。
这个仓库使用 strict TypeScript。
不要引入 any。外部输入先按 unknown 接收,并用 Zod 校验。
switch 处理 Union 时加入 never 穷尽检查。
实现后运行 npx tsc --noEmit。
noUncheckedIndexedAccess 会让数组和对象读取保留 undefined 的可能性。
它会让代码稍微严格一点,但能提前发现 API 字段缺失、空列表和 CMS 翻译漏项。
用例1:把 SaaS 套餐写成领域类型
领域类型就是把业务语言写成 TypeScript。 套餐、权限、账单状态、发布状态,都应该先于 UI 代码存在。
export type Plan = "free" | "pro" | "enterprise";
export type Account = {
id: string;
email: string;
plan: Plan;
seats: number;
trialEndsAt: string | null;
};
export type CreateAccountInput = {
email: string;
plan: Exclude<Plan, "enterprise">;
seats?: number;
};
export type UpdateAccountInput = Partial<
Pick<Account, "email" | "plan" | "seats" | "trialEndsAt">
>;
Exclude 从 Union 中移除成员。
Partial 把属性变成可选,适合更新 API,但不要直接拿来当新建输入,否则必填项也会被放松。
用例2:API 数据从 unknown 开始校验
TypeScript 类型在运行时不存在。
API、表单、Cookie、localStorage、CSV 和 AI 输出都可能是坏数据,所以边界处应使用 unknown,校验后再使用。
npm install zod
import { z } from "zod";
const AccountSchema = z.object({
id: z.string().min(1),
email: z.string().email(),
plan: z.enum(["free", "pro", "enterprise"]),
seats: z.number().int().positive(),
trialEndsAt: z.string().datetime().nullable()
});
type Account = z.infer<typeof AccountSchema>;
export function parseAccountResponse(json: unknown): Account {
return AccountSchema.parse(json);
}
unknown 表示“还没有证明它是什么”。
和 any 不同,它会强迫你先校验再读取属性。
用例3:用判别式 Union 封闭支付状态
付款、上传、表单提交和后台任务都是状态机。
不要写 status: string,因为不存在的状态也会混进来。
type PaymentResult =
| { status: "pending"; invoiceId: string }
| { status: "paid"; invoiceId: string; paidAt: string }
| { status: "failed"; invoiceId: string; reason: string };
export function renderPaymentMessage(result: PaymentResult): string {
switch (result.status) {
case "pending":
return `Invoice ${result.invoiceId} is waiting for payment.`;
case "paid":
return `Invoice ${result.invoiceId} was paid at ${result.paidAt}.`;
case "failed":
return `Invoice ${result.invoiceId} failed: ${result.reason}.`;
default: {
const exhaustive: never = result;
return exhaustive;
}
}
}
never 表示所有有效情况都应该已经处理完。
以后新增 refunded 状态时,如果忘记补分支,编译器会提醒。
用例4:用泛型和 satisfies 做复用 helper
泛型能让 helper 复用,同时保留调用处的具体类型。
export function groupBy<T, K extends PropertyKey>(
items: readonly T[],
getKey: (item: T) => K
): Partial<Record<K, T[]>> {
const grouped: Partial<Record<K, T[]>> = {};
for (const item of items) {
const key = getKey(item);
const bucket = grouped[key] ?? [];
bucket.push(item);
grouped[key] = bucket;
}
return grouped;
}
const accounts = [
{ id: "a1", plan: "free" },
{ id: "a2", plan: "pro" },
{ id: "a3", plan: "pro" }
] as const;
const byPlan = groupBy(accounts, (account) => account.plan);
const proAccounts = byPlan.pro ?? [];
console.log(proAccounts.map((account) => account.id));
配置对象适合使用 satisfies,不要随手 as SomeType。
type ApiRoute = {
method: "GET" | "POST" | "PATCH" | "DELETE";
path: `/${string}`;
auth: boolean;
};
const routes = {
listAccounts: { method: "GET", path: "/accounts", auth: true },
createAccount: { method: "POST", path: "/accounts", auth: true },
healthCheck: { method: "GET", path: "/health", auth: false }
} as const satisfies Record<string, ApiRoute>;
type RouteName = keyof typeof routes;
export function getRoute(name: RouteName) {
return routes[name];
}
加上类型测试
重要的导出类型需要测试。
Vitest 的 expectTypeOf 可以放进普通测试套件。
npm install -D vitest tsd
import { expectTypeOf, test } from "vitest";
type CreateAccountInput = {
email: string;
plan: "free" | "pro";
seats?: number;
};
test("CreateAccountInput keeps the public API narrow", () => {
expectTypeOf<CreateAccountInput>().toMatchTypeOf<{
email: string;
plan: "free" | "pro";
seats?: number;
}>();
});
实际用例和常见坑 (Use case / Pitfall checklist)
| 用例 | 类型系统负责 | Claude Code 可生成 |
|---|---|---|
| SaaS 计费 | plan、发票状态、权限 | UI 分支、表单、提示文案 |
| 管理后台 API | Zod schema、响应类型 | fetch、表格、加载状态 |
| 文章 CMS | slug、语言、发布状态、封面图 | MDX 草稿、列表页、校验修复 |
| 联系表单 | 输入 schema、提交结果 Union | UI、提交处理、Vitest 测试 |
| 坑 | 后果 | 修正 |
|---|---|---|
API 响应用 any | 错 JSON 也能编译 | 用 unknown 和 Zod |
status: string | 不存在的状态进入系统 | 用判别式 Union |
大量 as User | 只是隐藏错误 | 用 schema、类型守卫、satisfies |
新建输入直接用 Partial<T> | 必填项也变成可选 | 分开 create 和 update 类型 |
| 没有类型测试 | 导出类型悄悄变宽 | 加 expectTypeOf 或 tsd |
ClaudeCodeLab 的文章生成中,Masa 曾经因为 lang: string 太宽,生成了不存在的语言 URL。
把 locale 收紧成固定 Union 后,Claude Code 的修改建议明显更可靠。
如果要把这套做法放进真实项目,可以按三个 Use case 排优先级。第一是“外部输入边界”:表单、Webhook、CSV、第三方 API、AI 输出都先用 unknown 接收,再用 schema 校验。第二是“状态边界”:支付、上传、审批、发布流程都用判别式 Union 关闭状态。第三是“公开类型边界”:组件 props、SDK response、CMS frontmatter 这些会被很多文件引用的类型,必须用类型测试保护。
最常见的 Pitfall 是让 Claude Code 只追求编译通过。它可能会把错误改成 as User,或者把严格的 Union 放宽成 string。这时不要只写“修复类型错误”,而要写“不要放宽公开类型;外部输入必须先验证;新增状态必须补齐 switch 分支”。这样得到的 diff 更小,审查也更容易。
另一个容易忽略的点是迁移顺序。不要一次性改全仓库。先选一个 API helper 或一个表单,确认 tsc --noEmit、单元测试、类型测试都通过,再把同样的规则写进 CLAUDE.md。这样下一次会话里,Claude Code 会继续沿着同一套边界工作,而不是重新发明类型规则。
团队协作时,还要把“为什么这样收紧类型”写进提交说明或交接笔记。否则下一个人看到一堆编译错误,可能会把配置放松回去。我的做法是让 Claude Code 在完成报告里列出三项:新增的边界类型、仍然保留为 unknown 的输入、故意不使用 any 或 as 的原因。这个记录不长,但能让后续维护者理解类型不是装饰,而是保护业务规则的契约。
如果这三项说不清,先不要扩大修改范围,继续把边界缩小。
小边界更容易被验证,也更容易回滚。
CLAUDE.md 规则与 CTA
## TypeScript rules
- Use strict TypeScript.
- Do not introduce `any`. Use `unknown` at external boundaries.
- Prefer discriminated unions for states.
- Prefer `satisfies` over broad type assertions.
- Derive API types from Zod schemas when runtime data is involved.
- Add Vitest or tsd style type checks for exported helper types.
- Run `npx tsc --noEmit` before reporting completion.
个人项目可以从 ClaudeCodeLab 的产品列表拿到 Claude Code 模板和检查清单。
团队要在真实仓库里推进 strict 迁移、Zod 边界、类型测试和 CLAUDE.md 规则,可以看 Claude Code 培训与咨询。
实际验证
我在小型 TypeScript 项目中试过这套流程:把 API 响应从 any 改成 unknown 加 Zod,再让 Claude Code 补 Union 分支和 expectTypeOf 测试。
结果不是类型写得更花,而是在代码评审前就发现了漏处理状态和不存在的属性访问。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
Claude Code权限安全阶梯:逐步放开访问而不失控
从只读到有限编辑、验证命令和部署检查的 Claude Code 权限升级流程。
Claude Code 小PR证据包:让小改动真正可审查
用差异、验证命令、公开URL、CTA路径和回滚说明,把Claude Code的小PR变得可审查。
Claude Code 提交前 Review Gate:同时检查差异、测试、公开 URL 和 CTA
提交前用 Claude Code 审查差异范围、build、公开 URL、Gumroad 链接、咨询 CTA、缺少测试和无关文件。