Tips & Tricks (更新: 2026/6/3)

Claude Code 与 TypeScript:更快也更安全的实战技巧

用 strict、Zod、Union、泛型、satisfies 和类型测试提升 Claude Code 的 TypeScript 质量。

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 实现和复查"]

官方资料建议看 strictnoUncheckedIndexedAccessexactOptionalPropertyTypesNarrowingGenericsUtility Typessatisfies 说明。 运行时校验可以同时参考 Zod 文档

相关基础可以继续看 TypeScript Utility TypesTypeScript GenericsZod 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 分支、表单、提示文案
管理后台 APIZod schema、响应类型fetch、表格、加载状态
文章 CMSslug、语言、发布状态、封面图MDX 草稿、列表页、校验修复
联系表单输入 schema、提交结果 UnionUI、提交处理、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 的输入、故意不使用 anyas 的原因。这个记录不长,但能让后续维护者理解类型不是装饰,而是保护业务规则的契约。 如果这三项说不清,先不要扩大修改范围,继续把边界缩小。 小边界更容易被验证,也更容易回滚。

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 测试。 结果不是类型写得更花,而是在代码评审前就发现了漏处理状态和不存在的属性访问。

#Claude Code #TypeScript #类型安全 #开发效率 #frontend
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。