TypeScript 工具类型——从入门到高级的完整指南(含 Zod)
TypeScript 工具类型是预定义的泛型类型,用于简化类型转换。它们允许你基于已有类型创建新类型,例如将某些属性变为可选,或提取函数的返回类型。本指南分为三个阶段(入门、中级、高级),系统介绍所有标准 TypeScript 工具类型(如 Partial, Required, Readonly, Pick, Omit, Record, Exclude, Extract, NonNullable, Parameters, ReturnType, InstanceType, ThisType 等),每个类型都包含用途说明、TypeScript 示例,以及与 Zod(一个运行时校验库)下的等价方案对比。我们还将讨论 TypeScript 工具类型(静态类型系统)与 Zod 校验(运行时校验)之间的哲学差异。
使用 Bun 初始化项目
要尝试文中的代码片段,你可以用 Bun(一个超快的 JavaScript 运行器和包管理器)初始化一个最小的 TypeScript 项目。请确保已安装 Bun,然后在空文件夹下执行:
shell
Bun 会自动生成 package.json 和 tsconfig.json。你可以用 bun run file.ts 直接运行 TypeScript 文件(Bun 会自动转译 TypeScript)。
现在我们可以开始探索工具类型了!
入门:基础类型系统工具类型
Partial<T> —— 可选属性
TypeScript:Partial<T> 工具类型会将类型 T 的所有属性变为可选。也就是说,T 的每个属性都变成了可选(加上 ?)。这允许你表示该类型的部分对象。例如,如果你有一个 User 类型,所有字段都是必填的,Partial<User> 则允许你创建一个可以省略这些字段的版本:
typescript
此时 Partial<User> 的类型为 { name?: string; age?: number }。这让你可以实现只更新部分字段的函数,而无需所有字段都必填:
typescript
你可以用 updateUser(existingUser, { age: 26 }) 传入部分对象——TypeScript 静态保证 fieldsToUpdate 只包含 User 的有效键。
Zod 等价方案:Zod 提供了 .partial() 方法,可以让对象 schema 的所有键变为可选,类似 TypeScript 的 Partial。例如:
typescript
PartialUserSchema 校验的对象可以只包含部分键(name 和 age 都可选)。你可以这样测试:
typescript
Zod 也允许你只让部分属性可选,传入对象参数(UserSchema.partial({ age: true }) 只让 age 可选)。注意 .partial() 只作用于第一层,Zod 还提供 .deepPartial() 递归地让嵌套字段可选。
Required<T> —— 必填属性
TypeScript:Required<T> 工具类型与 Partial 相反:它会将类型 T 的所有属性变为必填(不可选)。这适用于将原本可选的类型"固化"为全部必填。例如:
typescript
此例中 Required<Settings> 等价于 { theme: string; fontSize: number },省略属性会导致编译错误。
Zod 等价方案:Zod 的 .required() 方法可以让对象 schema 的所有属性变为必填(与 .partial() 相反)。这在你先定义了部分可选 schema 后想要严格版本时很有用。例如:
typescript
StrictUserSchema 要求 name 和 age 必须存在。你也可以只指定部分属性:BaseUserSchema.required({ age: true }) 只让 age 必填(name 仍可选)。
注意 Required<T> 只在编译期生效(最终 JS 代码中不会有),而 Zod 的 schema.required() 会在运行时校验属性是否存在。
Readonly<T> —— 只读属性
TypeScript:Readonly<T> 工具类型会让类型 T 的所有属性变为只读(不可修改)。尝试重新赋值会在编译时报错。例如:
typescript
todo 是 Readonly<Todo> 类型,初始化后属性不可再修改。这类似于 JS 的 Object.freeze,但 TypeScript 只在类型层面限制(运行时不会阻止修改,只是编译期警告)。
Zod 等价方案:Zod 的 .readonly() 方法会返回一个新 schema,parse 时会对结果调用 Object.freeze(),并且类型上标记为 Readonly<...>。也就是说,Zod 不仅类型上只读,parse 后的对象在运行时也会被冻结。例如:
typescript
FrozenUserSchema 解析后对象被冻结。尝试修改 frozenUser 会抛出异常。Zod 比 TypeScript 更进一步:Readonly<T> 只在类型层面(JS 仍可修改),Zod 的 .readonly() 既有类型只读,也有运行时冻结。
Pick<T, Keys> —— 选择部分属性
TypeScript:Pick<T, Keys> 会从类型 T 中选出 Keys 指定的属性,生成新类型。适合从已有类型派生更精简的类型。例如:
typescript
TodoPreview 只包含 FullTodo 的 title 和 completed,未列出的属性(如 description)被排除。
Zod 等价方案:Zod 的 .pick() 方法可以从对象 schema 中选出指定键,生成新 schema,类似 TypeScript 的 Pick:
typescript
传入要保留的键,TodoPreviewSchema 只校验 title 和 completed(FullTodo 其他属性默认被忽略)。例如 TodoPreviewSchema.parse({ title: "Clean", completed: false, description: "..." }) 会返回 { title: "...", completed: false\ },description 被移除(Zod 默认忽略未声明的键,可用 .strict() 或 .passthrough() 调整行为)。
Zod 也有 .omit() 方法实现相反效果。
Omit<T, Keys> —— 排除部分属性
TypeScript:Omit<T, Keys> 会从类型 T 中排除 Keys 指定的属性,生成新类型。它是 Pick 的补集(通常 Omit<T, K> 实现为 Pick<T, Exclude<keyof T, K>>)。例如:
typescript
TaskMeta 只包含 id 和 title,done 和 dueDate 被排除。
Zod 等价方案:Zod 的 .omit() 方法会生成不包含指定键的新对象 schema。例如:
typescript
TaskMetaSchema 只校验 id 和 title。被省略的键(done, dueDate)即使出现在输入中也会被忽略。Pick 和 Omit 让你像 TypeScript 一样在 schema 层面投影字段。
Record<Keys, Type> —— 键值对字典
TypeScript:Record<Keys, Type> 会生成一个对象类型,键为 Keys(通常是字符串字面量联合类型或 string/number),值为 Type。适合描述以标识符为键的对象。例如:
typescript
Record<CatName, CatInfo> 保证 cats 对象有且仅有 "miffy"、"boris"、"mordred" 这几个键,每个键对应 CatInfo。Record 常用于类型安全的字典或映射。
Zod 等价方案:Zod 的 z.record(keySchema, valueSchema) 可校验此类对象。keySchema 可为 z.string() 或字面量 schema,valueSchema 为值类型。例如:
typescript
AgeMapSchema 校验所有键为字符串。若键集合有限,建议用 z.object({ ... }) 明确声明,或用 z.enum([...]) 限定键。Zod 允许更严格的键 schema:如 const CatNameSchema = z.enum(["miffy","boris","mordred"]),再用 z.record(CatNameSchema, CatInfoSchema)。注意 JS 对象键始终为字符串,数字键会被转为字符串。Zod 也会考虑这一点,不允许纯数字键 schema。
总结:Record<Keys, Type>(TypeScript)和 z.record(keySchema, valueSchema)(Zod)都可用于类型安全的字典。前者静态类型,后者运行时校验。
中级:联合类型与可选类型操作
Exclude<UnionType, ExcludedMembers> —— 从联合类型中排除成员
TypeScript:Exclude<U, E> 会从联合类型 U 中排除所有可赋值给 E 的成员。例如:
typescript
Exclude<Role, "admin"> 移除了 "admin",只剩 "user" | "guest"。再如 Exclude<string | number | boolean, string> 得到 number | boolean。常用于细化联合类型,排除不需要的情况。
Zod 等价方案:Zod 的枚举 schema(z.enum)有 .exclude([...]) 方法,可生成不包含指定值的新枚举 schema。例如:
typescript
NonAdminSchema 等价于 Exclude<Role, "admin">,并且运行时校验。Zod 也有 .extract([...]) 方法实现相反操作。
Extract<Type, Union> —— 提取联合类型成员
TypeScript:Extract<T, U> 会从 T 中提取所有可赋值给 U 的成员。与 Exclude 相反。例如:
typescript
再如 Extract<"a" | "b" | "c", "a" | "f"> 得到 "a"。常用于从联合类型中筛选特定子类型。
Zod 等价方案:Zod 的 .extract([...]) 方法可从枚举 schema 中提取指定值。例如:
typescript
LimitedRolesSchema 等价于 Extract<Role, "user" | "guest">。你也可以直接用 z.enum(["user","guest"]),但 .extract 适合从已有枚举派生。注意 .exclude 和 .extract 只适用于枚举 schema(z.enum 或 z.nativeEnum),复杂类型的联合需手动构造。
NonNullable<T> —— 排除 null 和 undefined
TypeScript:NonNullable<T> 会从类型 T 中排除 null 和 undefined。常用于联合类型中去除"空值"。例如:
typescript
此时 DefinitelyString 只包含 string。常与 if (value != null) { ... } 配合,帮助编译器收窄类型。
Zod 等价方案:Zod schema 默认不接受 undefined 或 null,除非你显式指定。z.string() 只接受字符串,不接受 undefined。要允许这些值需用 z.string().optional()(允许 undefined)、z.string().nullable()(允许 null)或 .nullish()(允许两者)。要获得 NonNullable 效果,只需不在 schema 中允许 null/undefined。如果你从更宽松的 schema 开始想收紧,可以用 .unwrap() 提取底层 schema:
typescript
OptionalName.unwrap() 返回原始 z.string()。这样可以"去除"可选或可为 null 的特性。自定义校验也可实现,但 Zod 默认已做了。
高级:函数类型与类类型
Parameters<Type> —— 函数参数类型
TypeScript:Parameters<T> 会提取函数类型 T 的参数类型,返回元组。适合处理函数签名,或包装函数。例如:
typescript
Parameters<typeof greet> 得到 [string, number],safeGreet 参数与 greet 完全一致。
Zod 等价方案:Zod 的 z.function() 可定义函数 schema,指定参数和返回类型。虽然没有直接等价的 Parameters<T>,但你可以用 TypeScript 的 Parameters 工具类型配合 z.infer 提取参数类型:
typescript
GreetSchema 定义了参数和返回类型,.implement() 创建带校验的包装函数。用 Parameters<GreetFn> 可提取参数类型,实现类型与运行时校验结合。
ReturnType<Type> —— 函数返回类型
TypeScript:ReturnType<T> 提取函数类型 T 的返回类型。适合复用函数返回值类型,无需重复定义。例如:
typescript
ReturnType<typeof createUser> 得到 createUser 的返回对象类型。
Zod 等价方案:同 Parameters,Zod 没有直接等价的 ReturnType<T>,但可结合 TypeScript 的 ReturnType 工具类型和 z.infer:
typescript
CreateUserSchema 定义函数 schema,可用 ReturnType<CreateUserFn> 或 .returnType() 提取返回类型。
InstanceType<Type> —— 类实例类型
TypeScript:InstanceType<T> 提取构造函数类型 T 的实例类型。适合有类引用时获取其实例类型。例如:
typescript
InstanceType<typeof Point> 得到 Point 实例类型,包含属性和方法。
Zod 等价方案:Zod 没有直接等价的类实例类型,因为它专注于数据校验而非类行为。但你可以定义一个 schema 匹配类实例的结构:
typescript
这种方式可校验对象结构是否符合类实例,但无法捕获完整的类关系。更复杂的类校验可结合 class-validator 等库。
TypeScript 工具类型与 Zod 校验的哲学差异
TypeScript 工具类型和 Zod schema 都用于保证数据可靠性,但作用层级不同:一个在编译期,一个在运行时。
- TypeScript:静态安全(编译期) —— 工具类型(以及整个类型系统)在开发阶段生效。TypeScript 会分析代码,在代码运行前阻止类型不一致。例如 Partial<T> 保证函数能处理属性缺失的情况,ReturnType<Fn> 给出函数返回值的结构等。但这些检查在应用运行后就不存在了:TypeScript 转译为 JavaScript 时会移除类型。运行时无法阻止无效数据(如 JSON、表单、外部 API)。类型系统本身无法防止运行时异常数据。
- Zod:动态校验(运行时) —— Zod schema 补足了运行时的空白。定义 schema 后,你可以在运行时校验数据结构是否符合预期。例如 PartialUserSchema 会实际校验对象的键和值类型,而 Partial<T> 只是编译期保证。Zod 和 TypeScript 是互补的:TypeScript 保证代码内部一致性,Zod 保证外部数据符合类型。
总结:
- TypeScript 是编译期守卫,能在开发早期发现错误,避免大多数常见问题。
- Zod 是运行时守卫,确保无效数据不会"闯入"你的应用(用户输入、API 响应、配置等)。
哲学上,工具类型表达类型系统层面的转换或约束(帮助开发者,对输出代码无影响),而 Zod 提供运行时契约(实现数据校验和转换)。两者解决不同但相关的需求:代码健壮性。Zod 还与 TypeScript 深度集成:可自动从 schema 推导静态类型,避免重复定义。你可以只写 Zod schema(用于校验),用 z.infer 推导 TypeScript 类型,无需再写 interface 和 schema 两套定义——数据类型只需一处声明。
结论:掌握 TypeScript 工具类型能让你充分利用类型系统,写出安全、表达力强的代码;精通 Zod 则能让你在运行时强力约束数据类型。两者结合极大提升 TypeScript 应用的可靠性:TypeScript 防止编程错误,Zod 防止外部异常数据。定义类型,校验数据,安心开发 🎉!