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

用 Claude Code 学 TypeScript Generics: keyof、约束与 API 类型

用 Claude Code 安全设计 TypeScript Generics,涵盖约束、keyof、mapped types、API 类型与 tsc 验证。

用 Claude Code 学 TypeScript Generics: keyof、约束与 API 类型

为什么不能只说“写成泛型”

TypeScript Generics 可以让同一个函数、类型或类适配多种数据结构,同时保留输入与输出之间的类型关系。初学者真正容易踩坑的地方,不是看不懂T这个字母,而是向 Claude Code 只说“帮我写一个通用函数”。这种提示太宽,模型很容易返回看似灵活、实际靠any或过宽的Record<string, unknown>撑起来的代码。

在真实项目里,泛型通常会碰到 API 数据、表单、账号、计费、埋点和商品 CTA。这里一旦类型过宽,问题不是编辑器补全不好看,而是后续调用方可能把错误字段传进收入路径。Masa 的经验是:凡是会影响表单提交、购买、咨询或统计事件的泛型,都要同时要求 Claude Code 写出“应该失败的例子”,再用tsc确认失败确实发生。

可以用下面的图理解本文的思路:

输入值 -> 用 T 捕获类型 -> 用 keyof T 限制可选字段 -> 用 mapped types 转换结构 -> 用 tsc 验证契约

本文的语法已参考 TypeScript 官方文档:Genericskeyof Type OperatorMapped TypesConditional Types。如果想补齐 Claude Code 的 TypeScript 工作流,可以继续读TypeScript 实践 TipsUtility Types 实战

先给 Claude Code 一张审查表

Generics 是编译期工具。T不是运行时变量,而是让编译器记住“传入了什么类型、应该返回什么类型”的类型参数。这里的extends更接近“只允许满足这个形状的类型”。keyof T会生成T可用属性名的集合。mapped types 则遍历这些属性名,生成一个新的对象类型。

写代码前,可以先把审查标准给 Claude Code:

问题给 Claude Code 的信息人工复查点
T代表什么业务对象、DTO、表单模型返回值没有丢失原始类型
哪些地方要限制K extends keyof TE extends ApiErrorT extends object非法调用在编译期失败
如何验证@ts-expect-errorExpect、严格tsc命令坏例子真的被拒绝

这张表能避免一个常见问题:Claude Code 写出能编译的代码,但只是最后加了强制类型转换。转换有时合理,比如 TypeScript 无法推断Object.fromEntries后的精确结构;但如果转换只是为了掩盖设计太宽,就应该退回重写。

用例1: 用keyof安全地去重

第一个用例是uniqueBy。它适合 API 列表、CSV 导入、管理后台表格和前端下拉选项。错误写法是把key写成string,这样调用方传不存在的字段也能通过编译。正确做法是用K extends keyof T,让key只能来自对象已有字段。

type User = {
  id: string;
  email: string;
  role: "admin" | "editor";
  score: number;
};

function uniqueBy<T>(items: readonly T[]): T[];
function uniqueBy<T, K extends keyof T>(items: readonly T[], key: K): T[];
function uniqueBy<T, K extends keyof T>(items: readonly T[], key?: K): T[] {
  const seen = new Set<unknown>();
  const output: T[] = [];

  for (const item of items) {
    const value = key === undefined ? item : item[key];
    if (seen.has(value)) continue;
    seen.add(value);
    output.push(item);
  }

  return output;
}

const users: User[] = [
  { id: "u_1", email: "masa@example.com", role: "admin", score: 92 },
  { id: "u_2", email: "editor@example.com", role: "editor", score: 88 },
  { id: "u_1", email: "masa+copy@example.com", role: "admin", score: 70 },
];

const byId = uniqueBy(users, "id");
const byRole = uniqueBy(users, "role");

// @ts-expect-error "missing" is not a key of User.
uniqueBy(users, "missing");

console.log(byId.map((user) => user.id));
console.log(byRole.map((user) => user.role));

给 Claude Code 的提示要明确写出:“使用 overload,key必须限制为keyof T,并加入不存在字段的@ts-expect-error测试。”否则模型可能写出key: string再配合item[key as keyof T],表面能过,实际把风险推迟到运行时。

用例2: 不用 optional 拼 API 响应

第二个用例是 API 响应类型。很多代码会把响应写成data?: Terror?: ApiError。这看起来省事,但调用方每次都要判断:成功时有没有data,失败时有没有error,甚至两者是否都不存在。用判别联合类型可以把状态写清楚:成功才有data,失败才有error,通过ok字段完成类型收窄。

type ApiError = {
  code: string;
  message: string;
  retryable: boolean;
};

type ApiResult<T, E extends ApiError = ApiError> =
  | { ok: true; status: number; data: T; error?: never }
  | { ok: false; status: number; error: E; data?: never };

type UserDto = {
  id: string;
  name: string;
  plan: "free" | "pro";
};

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

function parseUserResponse(json: unknown): ApiResult<UserDto> {
  if (
    isRecord(json) &&
    typeof json.id === "string" &&
    typeof json.name === "string" &&
    (json.plan === "free" || json.plan === "pro")
  ) {
    return {
      ok: true,
      status: 200,
      data: { id: json.id, name: json.name, plan: json.plan },
    };
  }

  return {
    ok: false,
    status: 422,
    error: {
      code: "INVALID_USER_RESPONSE",
      message: "User response does not match the expected shape.",
      retryable: false,
    },
  };
}

function unwrap<T, E extends ApiError>(result: ApiResult<T, E>): T {
  if (result.ok) {
    return result.data;
  }

  throw new Error(`${result.error.code}: ${result.error.message}`);
}

const parsed = parseUserResponse({ id: "u_1", name: "Masa", plan: "pro" });
const user = unwrap(parsed);
console.log(user.name.toUpperCase());

这个模式适合让 Claude Code 生成,因为提示词可以同时描述运行时校验和类型契约。需要更完整的后端上下文时,可以继续看Claude Code API 开发API 测试自动化

用例3: 用 mapped types 生成表单状态

第三个用例是表单状态。我们从业务模型出发,为每个字段生成valuedirtyerrors。mapped types 的好处是字段名不用手写两遍,字段值类型也不会丢失:email仍然是字符串,seats仍然是数字,newsletter仍然是布尔值。

type FieldState<T> = {
  value: T;
  dirty: boolean;
  errors: string[];
};

type FormState<T extends object> = {
  [K in keyof T]: FieldState<T[K]>;
};

function createFormState<T extends object>(initial: T): FormState<T> {
  const entries = Object.entries(initial).map(([key, value]) => [
    key,
    { value, dirty: false, errors: [] },
  ]);

  return Object.fromEntries(entries) as FormState<T>;
}

function setField<T extends object, K extends keyof T>(
  state: FormState<T>,
  key: K,
  value: T[K],
): FormState<T> {
  return {
    ...state,
    [key]: { value, dirty: true, errors: [] },
  } as FormState<T>;
}

type SignupForm = {
  email: string;
  seats: number;
  newsletter: boolean;
};

const form = createFormState<SignupForm>({
  email: "team@example.com",
  seats: 2,
  newsletter: true,
});

const updated = setField(form, "seats", 3);

// @ts-expect-error seats must be a number.
setField(form, "seats", "three");

console.log(updated.seats.value);

这里的复查点是Object.fromEntries后的类型转换。它不是在相信外部输入,而是在补充 TypeScript 无法推断的“原始键被完整保留”这一事实。请让 Claude Code 解释每一次as的理由,尤其是在团队协作的代码里。

tsc和类型测试验证

泛型文章最危险的写法,是代码看起来像真的,实际上没有通过编译。把上面的代码放到examples/generics.ts,再用严格模式检查。

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noEmit": true,
    "lib": ["ES2022", "DOM"]
  },
  "include": ["examples/**/*.ts"]
}
npm install --save-dev typescript
npx tsc --noEmit --strict --lib ES2022,DOM examples/generics.ts

如果只想测试类型,可以加入下面这种编译期断言。它不会在运行时做事,但只要类型不符合预期,tsc就会失败。

type Equal<A, B> =
  (<T>() => T extends A ? 1 : 2) extends
  (<T>() => T extends B ? 1 : 2)
    ? true
    : false;

type Expect<T extends true> = T;

type PickReadonly<T, K extends keyof T> = {
  readonly [P in K]: T[P];
};

type Account = {
  id: string;
  email: string;
  seats: number;
};

type PublicAccount = PickReadonly<Account, "id" | "email">;

type PublicAccountCheck = Expect<
  Equal<PublicAccount, { readonly id: string; readonly email: string }>
>;

const leaked: PublicAccount = {
  id: "a_1",
  email: "team@example.com",
  // @ts-expect-error seats is intentionally not part of PublicAccount.
  seats: 10,
};

console.log("Type checks are compile-time only.");

Claude Code 类型审查模板

生成代码后,再让 Claude Code 做一次专门的类型审查。下面的模板可以直接复制。

模板1: 泛型函数审查
请审查这个 TypeScript 函数。
目标: 按指定 key 对输入数组去重。
约束: key 必须是 K extends keyof T。禁止 any。为不存在的 key 加 @ts-expect-error。
输出: 问题点、修正版、验证用 tsc 命令。
模板2: API 响应类型审查
请审查这个 API 响应类型。
目标: 成功时只有 data,失败时只有 error。
约束: 避免 data?: T 这种模糊 optional 设计。确认 ok 能正确收窄类型。
输出: 安全调用例、失败例、应追加的类型测试。
模板3: mapped types 审查
请审查这个 mapped type。
目标: 从原始表单模型生成字段状态类型。
约束: 解释 keyof、T[K]、readonly、optional 属性和必要的类型转换。
输出: 类型流向、脆弱场景、最小安全修正。
模板4: PR 前类型审计
请审计这个 diff 中的 generics、conditional types 和 mapped types。
检查项: any、过宽的 Record、不必要的类型参数、缺少 @ts-expect-error、缺少运行时校验。
输出: 阻塞问题、轻微改进、追加测试,按优先级排列。

具体陷阱与处理方式

陷阱会发生什么更安全的做法
any伪装通用返回值失去有用信息T保留输入输出关系
把 key 写成string不存在字段也能编译K extends keyof T
过度使用Record<string, unknown>具体字段名消失不需要字典访问时用object
API 字段全写 optional调用方无法信任dataerror使用判别联合类型
不解释类型转换reviewer 无法判断安全性在转换前说明不变条件

T extends objectT extends Record<string, unknown>的选择尤其容易混淆。表单模型通常只需要是对象;真正处理任意字符串键的字典工具,才更适合Record。让 Claude Code 先说明取舍,再接受它生成的类型。

CTA: 把类型安全连接到收入路径

Generics 不是只给工程师看的语法游戏。表单、结账、API payload、商品模板和 analytics event 的类型过宽,都可能影响从读者到客户的路径。个人练习可以先拿免费 Claude Code cheatsheet固定日常命令;需要可复用提示词和设置材料时看ClaudeCodeLab 产品;团队要设计CLAUDE.md、类型审查、CI gate 和上线规则时,可以从培训与咨询开始。

把本文代码放进自己的仓库时,先找最靠近收入的类型:账号、计费、表单、API 响应、事件追踪。请 Claude Code 不只回答“能不能编译”,还要回答“这个类型错误会不会破坏转化路径”。

实际验证结果

实际试用后,Masa 发现把“生成实现”和“类型审查”拆开更稳定。第一轮让 Claude Code 写 helper,第二轮专门检查any、缺少keyof、API 结果 optional 过多、缺少@ts-expect-error等问题。uniqueBy和表单状态这两个例子尤其有效,因为tsc --noEmit --strict能同时证明有效调用会通过、故意写错的调用会失败。

#Claude Code #TypeScript #generics #type safety #design patterns
免费

免费 PDF: Claude Code 速查表

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

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

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

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

Masa

关于作者

Masa

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