如何在Zod中定义带默认值可选属性 解决类型与运行时不匹配问题
Zod带默认值可选字段类型与运行时不匹配问题
问题复现
使用Zod定义包含默认值的可选字段Schema时,出现类型推断与运行时逻辑矛盾:
- 预期:字段为可选属性,未传入或传入
undefined时自动应用预设默认值 - 实际:要么运行时默认值生效但TS类型推断字段为必填,要么类型标记为可选但运行时默认值不生效
复现代码如下:
import { Timestamp } from 'firebase/firestore'; import { z } from 'zod'; export const someSchema = z.object({ id: z.string(), timestamp: z.instanceof(Timestamp), type: z.enum(['fever', 'constipation']), notes: z.string().optional().default(''), }); export const someInput = someSchema .omit({ id: true }) .merge( z.object({ timestamp: z .date() .optional() .default(() => new Date()), }), ) .partial({ notes: true, }); export const schemaArray = z.array(someSchema); export type Schema = z.infer<typeof someSchema>; export type SchemaInput = z.infer<typeof someInput>; // 期望timestamp为可选,实际推断为必填 function a({ type, timestamp, notes}: SchemaInput){ someInput.parse({ type, timestamp, notes }) } a({type: 'fever'}) // TS报错:timestamp为必填字段
问题根源
- 类型用错:
z.infer获取的是Schema调用parse后的输出类型,带默认值的字段在输出时一定存在值,因此会被推断为必填;而传给parse的入参类型需要用z.input获取,这是很多人容易混淆的点。 - 冗余调用:
.default()本身已经内置了「入参为undefined或缺省时自动填充默认值」的逻辑,不需要提前链式调用.optional(),多余的.optional()会干扰.merge()、.partial()等方法的类型推断。 - 无效的partial调用:对已经设置了默认值的
notes字段单独调用.partial()是多余的,默认值逻辑本身就支持入参缺省。
修复方案
- 区分输入/输出类型:入参类型使用
z.input获取,parse后的返回值使用z.infer获取,不要混用。 - 移除
.default()前冗余的.optional()调用,简化Schema定义。 - 删除多余的
.partial()调用,默认值逻辑已经能满足入参可选的需求。
修复后的代码:
import { Timestamp } from 'firebase/firestore'; import { z } from 'zod'; export const someSchema = z.object({ id: z.string(), timestamp: z.instanceof(Timestamp), type: z.enum(['fever', 'constipation']), notes: z.string().default(''), }); export const someInput = someSchema .omit({ id: true }) .merge( z.object({ timestamp: z.date().default(() => new Date()), }), ); export const schemaArray = z.array(someSchema); export type Schema = z.infer<typeof someSchema>; // 入参类型使用z.input获取,此时timestamp、notes均为可选字段 export type SchemaInput = z.input<typeof someInput>; function a({ type, timestamp, notes}: SchemaInput){ // parse返回值为输出类型,timestamp、notes为必填值,直接使用即可 const parsedData = someInput.parse({ type, timestamp, notes }) } // 调用正常,无TS报错 a({type: 'fever'})
补充说明:如果需要对入参和出参做更灵活的类型裁剪,也可以在
.default()后手动调用.optional()标记输入侧可选,但优先使用官方提供的z.input/z.infer区分两类类型,能避免90%以上的默认值类型不匹配问题。
内容的提问来源于stack exchange,提问作者Danielo515
相关产品推荐
相关产品推荐

