跳到主要内容

创建日期:2026-09-15 | 最近更新:2026-09-15 本文结论均以本机实测为准:zod 4.5.4 / zod 3.25.76 对比、TypeScript 7.0.2(tsc。类型断言与基准数字都是真实运行结果,不是「据说」。

Zod 深潜 1:类型推导是怎么工作的

第 0 篇说「一份声明同时给类型和运行时校验」。这篇回答更底层的问题:z.infer 凭什么能推出来?为什么还有 z.inputz.output 两个?类型慢了怎么办?

1. 核心心智:schema 天生有「两个类型」

一个校验器天然有两个方向:喂进去的东西长什么样(输入)吐出来的东西长什么样(输出)。两者大多数时候一样,但一旦你用了 transformcoercedefault 就分叉了。

Zod 把这件事直接写进了泛型签名(v4,本机用 tsc 验证过):

// 参数顺序是 <Output, Input>
const s: z.ZodType<number, string> = z.string().transform((v) => v.length); // ✅ 编译通过

对应三个取类型的工具:

const S = z.object({ name: z.string() });

type A = z.infer<typeof S>; // 等价于 z.output<...>:校验“之后”的类型
type In = z.input<typeof S>; // 校验“之前”允许传进来的类型
type Out = z.output<typeof S>;

记一条z.infer 就是 z.output(校验后的结果类型)——因为绝大多数场景你关心的是「拿到手能安全用」的那个类型。

2. 哪些 API 会让 input ≠ output

API输入类型输出类型
z.string()stringstring
z.string().transform(s => s.length)stringnumber
z.coerce.number()unknownnumber
z.string().default('x')string | undefined(可省)string
z.string().optional()string | undefinedstring | undefined
z.string().refine(...)stringstring校验不改变类型
z.string().pipe(z.coerce.date())stringDate

这张表就是 z.input 存在的意义:像 RPC/表单这类「我要收一个输入、但内部想要另一种类型」的场景,参数用 z.input<typeof S>、返回值用 z.output<typeof S>,天然安全。

3. 实测:把推导结果「钉死」

类型是编译期的,光看代码没用——我写了断言让 tsc 来判:

const S = z.object({
n: z.coerce.number(), // 输入 unknown,输出 number
t: z.string().transform((s) => s.length), // 输入 string,输出 number
d: z.string().default('x'), // 输入可省,输出 string
});

type Out = z.output<typeof S>;
type In = z.input<typeof S>;

const out: Out = { n: 1, t: 2, d: 'x' }; // ✅ 输出:n/t 都是 number
const input: In = { n: '123', t: 'hello' }; // ✅ 输入:t 是 string,d 可省

// 用条件类型断言「推导结果精确等于什么」
type Assert<T extends true> = T;
type IsEqual<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
type _1 = Assert<IsEqual<Out, { n: number; t: number; d: string }>>;
type _2 = Assert<IsEqual<In, { n: unknown; t: string; d?: string }>>;

真实运行(npx tsc --noEmit --strict types/infer.ts):

(无输出,exit=0) ← 断言全部成立

而如果我故意把输出的 t 写成字符串:

const bad: Out = { t: 'string' }; // 输出应该是 number

tsc 立刻报(真实输出):

types/wrong.ts(4,20): error TS2322: Type 'string' is not assignable to type 'number'.

这两段就是「可验证的类型推导」:不是我给你画大饼,而是让编译期来判对错。你在项目里也可以用 IsEqual 这套断言给关键 schema 上锁(改坏了 CI 就红)。

4. 为什么推导能成立(机制一句话)

z.infer<S> 本质是 S extends ZodType<infer O, ...> ? O : never ——类型参数就挂在 schema 对象上infer 从泛型位置把它取出来。所以:

  • 你写 z.string() 时,返回类型的 Output 位置是 string
  • .transform((v: string) => v.length),新的 schema 类型变成 ZodType<number, string, ...>(Output 换了,Input 保留)——链条上每一步都在改这两个类型参数
  • 这就是为什么「类型不会猜」,它只是把泛型参数一层层传下去

想自己看:编辑器里 hover 一个 schema 变量,或者写 type T = typeof S 展开看。

5. 类型性能:v3 vs v4(本机实测)

大型项目里,schema 多了以后「类型推导慢」是真实痛点。zod v4 重写了类型系统。我做了个微基准:各生成 120 个嵌套 object schema(含 minarray.optionaltransform),分别用 v3 与 v4 编译,看 tsc 的统计:

指标zod 4.5.4zod 3.25.76变化
Types108,781112,238−3%
Instantiations210,547303,170−31%
Check time0.414s0.690s−40%
Total time0.469s0.745s−37%

实测命令与输出片段:

npx tsc --noEmit --strict --module esnext --moduleResolution bundler --extendedDiagnostics types/bench4.ts
# Types: 108781 / Instantiations: 210547 / Check time: 0.414s / Total time: 0.469s

怎么读这组数Instantiations(类型实例化次数)是「类型层面开销」最直接的代理指标,v4 少了近三分之一——这正是官方说「v4 类型更快」在你这台机器上的量化体现。

方法论声明(避免误导):这是微基准,不代表你项目的真实收益;TS 版本是 7.0.2;schema 形状、strict 设置、是否 skipLibCheck 都会影响数字。要评估自己的项目,就用同样的 --extendedDiagnostics 在你仓库里跑一遍。

6. 常见类型问题排查清单

症状原因 / 解法
type X = z.infer<typeof S> 报「S 是值不是类型」z.infer 必须配 typeof(取的是值的类型
赋值报 string 不能给 number你用了 transform/coerce,输出已变 → 用 z.input 收参数、z.output 接结果
对象里字段明明可选却报缺少.optional() 只管该字段本身;嵌套里要让整个子对象可省,得写在外层
声明成 z.ZodType<X> 后推断丢了显式标注会抹掉具体类型信息(只保留你写的那个)——需要标注时写全 z.ZodType<Output, Input>
类型检查突然变慢schema 太大/太深、大量 .extend() 链、或 v3 的类型开销 → 考虑拆 schema(用 z.object() 组合)或升级 v4
想要「带名字的类型」z.brand() / z.custom<T>() 可做 branded 类型(进阶用法)

7. 一句话总结

  • z.infer = z.output,产出「校验后」的类型;z.input 是「允许传进来」的类型;
  • 一旦用 transform / coerce / default / pipe,两者就分叉——这不是坑,是特性
  • 推导不是魔法,是泛型参数在链上传递,所以你可以用类型断言把它钉死;
  • 规模上来后,v4 的类型开销明显低于 v3(本机 −31% 实例化),这也是升级的硬理由之一。

关联

自测

  1. z.inferz.inputz.output 三者关系?
  2. 哪些 API 会让输入输出类型分叉?refine 会吗?
  3. 怎么在项目里「锁住」一个 schema 的推导结果?
  4. Instantiations 这个指标说明什么?本机 v3→v4 变了多少?
  5. 给 schema 写显式类型标注有什么副作用?