分享这篇文章
Sébastien TIMONER
作为 Web 开发和技术团队管理专家,我专注于创建和优化高性能数字解决方案。通过对 React.js、Node.js、TypeScript、Symfony、Docker 和 FrankenPHP 等现代技术的深入掌握,我确保为各行业企业的复杂 SaaS 项目从设计到生产的成功。
TypeScript 工具类型是预定义的泛型类型,用于简化类型转换。它们允许你基于已有类型创建新类型,例如将某些属性变为可选,或提取函数的返回类型。本指南分为三个阶段(入门、中级、高级),系统介绍所有标准 TypeScript 工具类型(如 Partial, Required, Readonly, Pick, Omit, Record, Exclude, Extract, NonNullable, Parameters, ReturnType, InstanceType, ThisType 等),每个类型都包含用途说明、TypeScript 示例,以及与 Zod(一个运行时校验库)下的等价方案对比。我们还将讨论 TypeScript 工具类型(静态类型系统)与 Zod 校验(运行时校验)之间的哲学差异。
要尝试文中的代码片段,你可以用 Bun(一个超快的 JavaScript 运行器和包管理器)初始化一个最小的 TypeScript 项目。请确保已安装 Bun,然后在空文件夹下执行:
shell
Bun 会自动生成 package.json 和 tsconfig.json。你可以用 bun run file.ts 直接运行 TypeScript 文件(Bun 会自动转译 TypeScript)。
现在我们可以开始探索工具类型了!
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() 递归地让嵌套字段可选。
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() 会在运行时校验属性是否存在。
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() 既有类型只读,也有运行时冻结。
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() 方法实现相反效果。
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 层面投影字段。
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)都可用于类型安全的字典。前者静态类型,后者运行时校验。
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([...]) 方法实现相反操作。
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),复杂类型的联合需手动构造。
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 默认已做了。
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> 可提取参数类型,实现类型与运行时校验结合。
TypeScript:ReturnType<T> 提取函数类型 T 的返回类型。适合复用函数返回值类型,无需重复定义。例如:
typescript
ReturnType<typeof createUser> 得到 createUser 的返回对象类型。
Zod 等价方案:同 Parameters,Zod 没有直接等价的 ReturnType<T>,但可结合 TypeScript 的 ReturnType 工具类型和 z.infer:
typescript
CreateUserSchema 定义函数 schema,可用 ReturnType<CreateUserFn> 或 .returnType() 提取返回类型。
TypeScript:InstanceType<T> 提取构造函数类型 T 的实例类型。适合有类引用时获取其实例类型。例如:
typescript
InstanceType<typeof Point> 得到 Point 实例类型,包含属性和方法。
Zod 等价方案:Zod 没有直接等价的类实例类型,因为它专注于数据校验而非类行为。但你可以定义一个 schema 匹配类实例的结构:
typescript
这种方式可校验对象结构是否符合类实例,但无法捕获完整的类关系。更复杂的类校验可结合 class-validator 等库。
TypeScript 工具类型和 Zod schema 都用于保证数据可靠性,但作用层级不同:一个在编译期,一个在运行时。
总结:
哲学上,工具类型表达类型系统层面的转换或约束(帮助开发者,对输出代码无影响),而 Zod 提供运行时契约(实现数据校验和转换)。两者解决不同但相关的需求:代码健壮性。Zod 还与 TypeScript 深度集成:可自动从 schema 推导静态类型,避免重复定义。你可以只写 Zod schema(用于校验),用 z.infer 推导 TypeScript 类型,无需再写 interface 和 schema 两套定义——数据类型只需一处声明。
结论:掌握 TypeScript 工具类型能让你充分利用类型系统,写出安全、表达力强的代码;精通 Zod 则能让你在运行时强力约束数据类型。两者结合极大提升 TypeScript 应用的可靠性:TypeScript 防止编程错误,Zod 防止外部异常数据。定义类型,校验数据,安心开发 🎉!