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

TypeScript 实用工具类型入门:用 Claude Code 做安全类型设计

用可运行示例讲清 Pick/Omit/Partial/Record/ReturnType/Awaited,帮你用 Claude Code 做安全类型设计。

TypeScript 实用工具类型入门:用 Claude Code 做安全类型设计

TypeScript 的实用工具类型可以理解为“用已有类型加工出另一个用途明确的类型”。 如果你把一个 User 类型手动复制成“公开展示”“表单输入”“API 更新”三份,字段一变就很容易不一致。 Claude Code 能帮助重构这些类型,但你需要先理解每个工具的意图,才能判断它生成的代码是否安全。

本文用初学者容易理解的方式解释 PickOmitPartialRequiredReadonlyRecordReturnTypeAwaited。 随后用可直接复制运行的例子,展示如何让 Claude Code 把这些类型落到实际项目里。 官方定义请以 TypeScript Handbook Utility Types 为准;如果希望更早发现类型问题,也建议打开 TSConfig strict

相关内容可以继续看 ClaudeCodeLab 的 TypeScript 实践技巧TypeScript 泛型指南

先用白话理解

实用工具类型就是“安全地复制并改造类型”。 它有点像复制一张表格,然后隐藏某些列,把某些列改成可选,再交给另一个流程使用。 区别在于,TypeScript 会在代码运行前检查这个改造后的类型是否合理。

Pick<User, "id" | "name"> 表示只从 User 里挑出 idnameOmit<User, "passwordHash"> 表示保留大部分字段,但排除 passwordHash。 两者很像,但阅读方向不同:目标类型很小就用 Pick,目标类型几乎等于原类型、只是去掉少数字段时用 Omit 更清楚。

Partial<User> 会把所有属性变成可选,适合草稿、PATCH 请求和填写中的表单。 但它也可能让创建用户时必须有的 email 变成可选,这是常见坑。 Required<User> 则相反,会把可选属性变成必填。 Readonly<User> 用来表达“不应被重新赋值”,适合配置、权限表和固定主数据。

Record<Keys, Type> 用来创建“键集合固定的字典”。 ReturnType<typeof fn> 取出函数返回值类型。 Awaited<Promise<T>> 取出 await 之后得到的值类型。 把 AwaitedReturnType 组合起来,就能从 API 函数本身导出页面需要的结果类型。

flowchart LR
  A["Source type: User"] --> B["Pick: public view"]
  A --> C["Omit: remove secrets"]
  A --> D["Partial: update input"]
  A --> E["Required: validated input"]
  A --> F["Readonly: fixed settings"]
  G["Function"] --> H["ReturnType"]
  I["Promise"] --> J["Awaited"]

快速对照表

类型作用常见场景注意点
Pick<T, K>只选择指定字段列表、公开资料、卡片没选的字段不能直接使用
Omit<T, K>排除指定字段创建输入、公开输出、日志不会删除运行时对象里的值
Partial<T>所有字段变可选草稿、PATCH、表单中间状态只作用于第一层
Required<T>所有字段变必填保存前的已验证数据可能把表单要求设得过重
Readonly<T>禁止重新赋值配置、权限、常量深层对象要额外处理
Record<K, T>固定键的字典角色权限、文案、价格表Record<string, T> 往往太宽
ReturnType<T>取函数返回值类型同步 API 和 UI 类型要写 typeof functionName
Awaited<T>取 Promise 解析后的类型async 函数结果不是运行时的 await

把这张表贴给 Claude Code,再要求它按照用途审查类型选择,通常比简单说“优化类型”更有效。 在 strict: true 的项目中,模糊类型会更早暴露,修复成本也更低。

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "skipLibCheck": true
  }
}

用例1:从 User 派生页面和表单类型

管理后台经常需要同一个实体的多个版本。 数据库类型、公开展示类型、表单输入类型如果手写维护,字段迟早会漂移。 更稳妥的方式是保留一个源类型,再用工具类型派生其他用途。

type UserRole = "admin" | "editor" | "viewer";

interface User {
  id: string;
  name: string;
  email: string;
  role: UserRole;
  bio: string;
  passwordHash: string;
  createdAt: Date;
  updatedAt: Date;
}

type PublicUser = Pick<User, "id" | "name" | "role" | "bio">;
type UserDraft = Partial<Omit<User, "id" | "passwordHash" | "createdAt" | "updatedAt">>;

type CreateUserInput =
  Required<Pick<User, "name" | "email" | "role">> &
  Partial<Pick<User, "bio">>;

function buildCreatePayload(input: CreateUserInput): Omit<User, "id" | "createdAt" | "updatedAt"> {
  return {
    name: input.name,
    email: input.email,
    role: input.role,
    bio: input.bio ?? "",
    passwordHash: "hashed-by-server",
  };
}

const publicUser: PublicUser = {
  id: "u_001",
  name: "Masa",
  role: "admin",
  bio: "Claude Code workflow designer",
};

const draft: UserDraft = {
  name: "Draft user",
  bio: "Saved before email is confirmed",
};

console.log(publicUser);
console.log(buildCreatePayload({ name: "Aki", email: "aki@example.com", role: "editor" }));
console.log(draft);

给 Claude Code 的提示要把“保留什么、删除什么、何时必填”说清楚。

Create public display, form draft, and create-API types from the User type.
Do not expose passwordHash.
For creation, require only name, email, and role. Keep bio optional.
Use Pick/Omit/Partial/Required and briefly explain why each utility type is used.

实际项目中还要配合 Zod 等运行时校验。 实用工具类型只能在编译阶段检查形状,不能证明用户提交的数据一定可信。

用例2:用 Record 固定套餐功能

套餐、角色、状态表很适合 Record。 它可以在编译时发现漏写 team 套餐,或把 prioritySupport 拼错的问题。

type Plan = "free" | "pro" | "team";
type Feature = "exportPdf" | "inviteMember" | "prioritySupport";

const featureMatrix: Readonly<Record<Plan, Readonly<Record<Feature, boolean>>>> = {
  free: {
    exportPdf: false,
    inviteMember: false,
    prioritySupport: false,
  },
  pro: {
    exportPdf: true,
    inviteMember: false,
    prioritySupport: false,
  },
  team: {
    exportPdf: true,
    inviteMember: true,
    prioritySupport: true,
  },
};

function canUse(plan: Plan, feature: Feature): boolean {
  return featureMatrix[plan][feature];
}

console.log(canUse("pro", "exportPdf"));
console.log(canUse("free", "prioritySupport"));

这里使用 Readonly 是为了把“这张表不该被修改”的意图写进类型。 Readonly 默认偏浅,所以示例里内层 Record 也加了 Readonly。 如果数据层级更深,可以考虑 as const 或项目内统一的深层只读工具类型。

用例3:用 ReturnType 和 Awaited 复用 API 结果

API 客户端和页面组件如果各自手写类型,响应字段变化时很难维护。 ReturnTypeAwaited 可以直接从 async 函数导出结果类型。

async function fetchInvoice(invoiceId: string) {
  return {
    id: invoiceId,
    status: "paid" as const,
    amount: 48000,
    currency: "JPY" as const,
    paidAt: new Date("2026-06-02T10:00:00+09:00"),
  };
}

type Invoice = Awaited<ReturnType<typeof fetchInvoice>>;
type InvoiceSummary = Pick<Invoice, "id" | "status" | "amount" | "currency">;

function formatInvoice(invoice: InvoiceSummary): string {
  return `${invoice.id}: ${invoice.amount.toLocaleString()} ${invoice.currency} (${invoice.status})`;
}

async function main() {
  const invoice = await fetchInvoice("inv_20260602");
  console.log(formatInvoice(invoice));
}

main();

当 API 函数是可信边界时,可以要求 Claude Code 不要再手写重复的响应类型,而是从函数导出。 如果边界是外部 API 或用户输入,仍然要加运行时校验和错误处理。

用例4:在正确层级使用 Partial

Partial<T> 是浅层的,不会让嵌套对象里的字段自动变可选。 这是初学者最容易踩到的坑之一。

interface Profile {
  id: string;
  displayName: string;
  settings: {
    emailNotification: boolean;
    smsNotification: boolean;
  };
}

type ProfilePatch =
  Omit<Partial<Profile>, "settings"> & {
    settings?: Partial<Profile["settings"]>;
  };

function patchProfile(current: Profile, patch: ProfilePatch): Profile {
  return {
    ...current,
    ...patch,
    settings: {
      ...current.settings,
      ...patch.settings,
    },
  };
}

const profile: Profile = {
  id: "p_001",
  displayName: "Masa",
  settings: {
    emailNotification: true,
    smsNotification: false,
  },
};

console.log(patchProfile(profile, { settings: { smsNotification: true } }));

如果只写 ProfilePatch = Partial<Profile>,更新 settings 时仍然需要完整的 settings 对象。 对团队新人来说,像这样写一个具体的补丁类型,往往比设计很聪明的通用深层 Partial 更容易维护。

具体失败例

Omit 只会从类型层面移除字段,不会从运行时对象里删除属性。 如果要返回日志或公开 API 响应,必须真的把敏感值剥离出去。

interface Account {
  id: string;
  email: string;
  passwordHash: string;
}

type SafeAccount = Omit<Account, "passwordHash">;

function toSafeAccount(account: Account): SafeAccount {
  const { passwordHash, ...safeAccount } = account;
  return safeAccount;
}

console.log(toSafeAccount({
  id: "a_001",
  email: "masa@example.com",
  passwordHash: "secret",
}));

Record<string, T> 对业务规则通常太宽。 如果键集合已知,优先使用 type Plan = "free" | "pro" | "team" 这样的联合类型。

Required<T> 不适合太早用于表单。 它更适合在验证完成之后,表达“保存前已经齐全”的数据。

Awaited<T> 只是描述 Promise 解析后的类型,并不会在运行时等待。 如果把这个概念混淆,Claude Code 可能只整理了类型,却漏掉 loading 和 error 状态。

让 Claude Code 做审查的提示

实现后,不要只说“把类型变简洁”。 用下面的提示让 Claude Code 从风险角度审查。

Review this TypeScript type design.
1. Are Pick/Omit/Partial/Required/Readonly/Record matched to their use cases?
2. Are secrets removed at runtime, not only with Omit?
3. Does Partial make create inputs too loose?
4. Do ReturnType and Awaited reduce duplicated API types?
5. Are any vague any or broad string types left under strict settings?

这个提示关注的是事故预防,而不是类型写得多漂亮。 Masa 在一个小型管理页面里也遇到过类似问题:到处使用 Partial,导致空的 email 在保存流程前看起来也“合法”。 后来把“草稿”“创建”“已保存”拆成不同派生类型,Claude Code 给出的修改也更容易 review。

总结

TypeScript 实用工具类型不是炫技。 它们的价值在于:不用反复手写相似类型,而是清楚表达公开数据、草稿、已验证输入、固定配置和 async API 结果之间的差异。 用 PickOmit 控制字段,用 PartialRequired 表达流程阶段,用 ReadonlyRecord 防止配置漏项,用 ReturnTypeAwaited 减少响应类型重复。

ClaudeCodeLab 可以帮助团队把 Claude Code 应用于 TypeScript 架构、内容 CMS、内部工具和可变现产品漏斗。 如果你的项目“有类型但还是容易出事故”,可以通过 Claude Code 培训与咨询 联系我们。

本文中的示例按 strict TypeScript 的思路做了验证,最大的收益来自 ReturnTypeAwaited:API 响应变化时,页面侧需要调整的位置更早暴露。 同时也要记住,Omit 永远不会删除运行时的秘密字段,公开响应函数仍然必须显式剥离对象属性。

#Claude Code #TypeScript #utility types #type safety #代码质量
免费

免费 PDF: Claude Code 速查表

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

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

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

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

Masa

关于作者

Masa

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