泛型Zod Schema作为参数时,扩展运算符绕过TypeScript类型检查问题
解决Zod Schema与对象的严格类型检查问题
先重现你的问题场景,方便理解:
import { z, ZodSchema } from "zod"; const UserSchema = z.object({ name: z.string(), age: z.number(), }); type User = z.infer<typeof UserSchema>; // 原始函数 function myTestFunc<T extends ZodSchema>(schema: T, newItem: z.infer<T>) { schema.parse(newItem); } // 直接传字面量有类型错误(符合预期) myTestFunc(UserSchema, { name: "Alice", age: 30, city: "Paris" }); // ❌ 错误:对象字面量只能指定已知属性 // 扩展运算符传入无错误(问题所在) myTestFunc(UserSchema, { ...{ name: "Alice", age: 30 }, city: "Paris" }); // ✅ 无错误,但实际不符合Schema // 封装参数的写法同样失效 function myTestFunc2<T extends ZodSchema>(args: { schema: T; newItem: z.infer<T> }) { args.schema.parse(args.newItem); } const brokenItem = { name: "Bob", age: 25, city: "London" }; myTestFunc2({ schema: UserSchema, newItem: brokenItem }); // ✅ 无错误,但存在额外属性
问题原因
TypeScript默认采用结构子类型:只要对象包含目标类型的所有必填属性,就认为它符合类型,允许存在额外属性。而仅当直接传入对象字面量时,才会触发额外属性检查。当对象是变量、或通过扩展运算符生成时,这个检查会被跳过,因为TypeScript认为这是“已存在的对象”,不是新鲜的字面量。
解决方案
要实现严格的类型检查,需强制让传入的对象完全匹配Zod Schema推断出的类型,不允许任何额外属性。可以通过定义Exact类型来实现:
1. 定义Exact类型约束
import { z, ZodSchema } from "zod"; // Exact类型:确保U和T完全一致,无额外属性 type Exact<T, U> = T extends U ? (U extends T ? T : never) : never; // 修改函数,用Exact约束newItem类型 function myTestFunc<T extends ZodSchema>(schema: T, newItem: Exact<z.infer<T>, typeof newItem>) { schema.parse(newItem); }
现在测试所有场景都会触发正确的类型错误:
myTestFunc(UserSchema, { name: "Alice", age: 30, city: "Paris" });❌ 类型错误myTestFunc(UserSchema, { ...{ name: "Alice", age: 30 }, city: "Paris" });❌ 类型错误const brokenItem = { name: "Bob", age: 25, city: "London" }; myTestFunc(UserSchema, brokenItem);❌ 类型错误
2. 配合Zod的strict()方法(可选)
如果你的业务场景需要在运行时也严格校验无额外属性,可以给Schema添加strict(),再结合上面的类型约束:
const StrictUserSchema = UserSchema.strict(); myTestFunc(StrictUserSchema, { name: "Alice", age: 30, city: "Paris" }); // 编译时报类型错误,运行时调用schema.parse也会抛出Zod错误
原理说明
Exact<T, U>类型通过双向约束,确保U既是T的子类型,T也是U的子类型,从而强制两者的结构完全一致,彻底绕过TypeScript的结构子类型宽容性,实现严格的类型匹配。
内容的提问来源于stack exchange,提问作者Tom Roberts
相关产品推荐
相关产品推荐

