用 Claude Code 学 TypeScript Generics: keyof、约束与 API 类型
用 Claude Code 安全设计 TypeScript Generics,涵盖约束、keyof、mapped types、API 类型与 tsc 验证。
为什么不能只说“写成泛型”
TypeScript Generics 可以让同一个函数、类型或类适配多种数据结构,同时保留输入与输出之间的类型关系。初学者真正容易踩坑的地方,不是看不懂T这个字母,而是向 Claude Code 只说“帮我写一个通用函数”。这种提示太宽,模型很容易返回看似灵活、实际靠any或过宽的Record<string, unknown>撑起来的代码。
在真实项目里,泛型通常会碰到 API 数据、表单、账号、计费、埋点和商品 CTA。这里一旦类型过宽,问题不是编辑器补全不好看,而是后续调用方可能把错误字段传进收入路径。Masa 的经验是:凡是会影响表单提交、购买、咨询或统计事件的泛型,都要同时要求 Claude Code 写出“应该失败的例子”,再用tsc确认失败确实发生。
可以用下面的图理解本文的思路:
输入值 -> 用 T 捕获类型 -> 用 keyof T 限制可选字段 -> 用 mapped types 转换结构 -> 用 tsc 验证契约
本文的语法已参考 TypeScript 官方文档:Generics、keyof Type Operator、Mapped Types、Conditional Types。如果想补齐 Claude Code 的 TypeScript 工作流,可以继续读TypeScript 实践 Tips和Utility Types 实战。
先给 Claude Code 一张审查表
Generics 是编译期工具。T不是运行时变量,而是让编译器记住“传入了什么类型、应该返回什么类型”的类型参数。这里的extends更接近“只允许满足这个形状的类型”。keyof T会生成T可用属性名的集合。mapped types 则遍历这些属性名,生成一个新的对象类型。
写代码前,可以先把审查标准给 Claude Code:
| 问题 | 给 Claude Code 的信息 | 人工复查点 |
|---|---|---|
T代表什么 | 业务对象、DTO、表单模型 | 返回值没有丢失原始类型 |
| 哪些地方要限制 | K extends keyof T、E extends ApiError、T extends object | 非法调用在编译期失败 |
| 如何验证 | @ts-expect-error、Expect、严格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?: T和error?: 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 生成表单状态
第三个用例是表单状态。我们从业务模型出发,为每个字段生成value、dirty、errors。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 | 调用方无法信任data或error | 使用判别联合类型 |
| 不解释类型转换 | reviewer 无法判断安全性 | 在转换前说明不变条件 |
T extends object和T 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能同时证明有效调用会通过、故意写错的调用会失败。
免费 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、缺少测试和无关文件。