在对.strict() 进行换行时,输入会中断

前端开发 2026-07-09

我需要根据某些条件应用 .strict(),因此使用如下的包装器:

const strictIfTesting = <T extends z.ZodObject>(schema: T) => {
    const isProductionEnvironment = "window" in globalThis
    return isProductionEnvironment ? schema : schema.strict() as T
}

问题出在 as T 的强制转换,这只是一个丑陋的权宜之计,用来让类型检查通过。

这样就能工作:

const mySchema = () => strictIfTesting(z.object({
    someKey: strictIfTesting(z.object({
        anotherKey: z.string().nonempty().nonoptional()
    })).nonoptional()
}))

const strictIfTesting = <T extends z.ZodObject>(schema: T) => {
    const isProductionEnvironment = "window" in globalThis
    return isProductionEnvironment ? schema : schema.strict() as T
}

const myObject = mySchema().parse({})
console.log(myObject.someKey.anotherKey) // Everything works fine

但这就不行:

const mySchema = () => strictIfTesting(z.object({
    someKey: strictIfTesting(z.object({
        anotherKey: z.string().nonempty().nonoptional()
    })).nonoptional()
}))

const strictIfTesting = <T extends z.ZodObject>(schema: T) => {
    const isProductionEnvironment = "window" in globalThis
    return isProductionEnvironment ? schema : schema.strict()
}

const myObject = mySchema().parse({})
console.log(myObject.someKey.anotherKey) // Not compiles: myObject.someKey' is of type 'unknown'

如果放弃 strictIfTesting(),改为直接使用 .strict(),一切都按预期工作:

const mySchema = () => z.object({
    someKey: z.object({
        anotherKey: z.string().nonempty().nonoptional()
    }).strict().nonoptional()
}).strict()

const myObject = mySchema().parse({})
console.log(myObject.someKey.anotherKey) // Everything works fine

所以很明显,我在定义 strictIfTesting() 的类型时做错了。如何在不使用丑陋变通办法的情况下实现条件式的严格验证?

解决方案

TL;DR:让你的函数在模式的 shape 的类型上泛化,而不是在模式对象本身上泛化:

const strictIfTesting = <S extends z.core.$ZodShape>(schema: z.ZodObject<S>) => {
  const isProductionEnvironment = "window" in globalThis
  return isProductionEnvironment ? schema : schema.strict()
}

const mySchema = () => strictIfTesting(z.object({
  someKey: strictIfTesting(z.object({
    anotherKey: z.string().nonempty().nonoptional()
  })).nonoptional()
}))

const myObject = mySchema().parse({});
// const myObject: { someKey: { anotherKey: string; };}

一个初步的警告:我对Zod并不了解太多。不过我对TypeScript比较熟,所以就谈谈这个话题。如果有某种“更合适”的Zod做法与我下面的说法不一致,请直接采用那种做法。


你的问题在于,当你对一个泛型类型的值调用方法时,TypeScript会在评估类型之前把泛型从具体类型扩展到它的约束。请参阅关于microsoft/TypeScript#33181的注释。在你的情形下,T 是泛型,在调用 strict() 之前会被扩展成 z.ZodObject

更基础的例子:假设你想编写一个函数,它只接收一个数组并返回它的拷贝,而且希望它是【泛型】的。也许你会写成:

function copyArr<T extends unknown[]>(t: T) { return t.slice() }
// function copyArr<T extends unknown[]>(t: T): unknown[]
const s = copyArr(["a"]).map(x => x.toUpperCase()) // error!

发生了什么?即使 copyArr() 在参数 t 的类型 T extends unknown[] 上是泛型,它返回的却只是 unknown[],而不是 T。因为 t 会在你调用 slice() 之前从 T 扩展到 unknown[],从而抹去了关于数组元素类型的任何信息。因此 copyArr(["a"]) 的类型只是 unknown[],TypeScript也就不知道里面其实有字符串。


你的变通办法是进行一个 [类型断言]。对于数组的示例,看起来像:

function copyArr<T extends unknown[]>(t: T) {
  return t.slice() as T
}
const s = copyArr(["a"]).map(x => x.toUpperCase()); // no error, but
const oops = copyArr(Object.assign([], { a: 1 })).a.toFixed(); // explodes at runtime

现在 copyArr() 返回类型为 T,所以 copyArr(["a"]) 的类型是 string[]。但类型断言本质上本来就不安全(或至少比TypeScript的其他部分更不安全)。你可以看到上面的 oops……我传给 copyArr() 的参数是一个空数组,给它额外加上了一个 {a: number} 属性。(在JS/TS中通常可以这样做,给大多数对象、数组也可以加属性。)于是TypeScript也会认为返回值还有一个 {a: number} 属性,并且会让你自由访问它,而在运行时却会崩溃,因为 slice() 只复制数组的“元素”,不复制任何额外属性。确实,slice() 的类型从来没有声称返回与输入完全相同的类型;相反,它看起来像 interface Array<E> {slice(): Array<E>}:它只保证返回一个元素类型相同的数组。

同样的道理大概也适用于 ZodObject<S>strict() 方法。它返回另一个 ZodObject<S>,不一定是初始值的“同一类型”。如果 T extends ZodObject 某种情况下更窄(例如包含额外属性),那么你就不想把返回类型写成 T


所以我们不要再使用那种变通办法。真正想做的是保留 shape 属性的类型……类似于 z.ZodObject<T["shape"]>,而不是 T。对于数组而言,看起来像:

function copyArr<T extends unknown[]>(t: T) {
  const ret: Array<T[number]> = t.slice();
  return ret;
}

const s = copyArr(["a"]).map(x => x.toUpperCase()); // yay
const oops = copyArr(Object.assign([], { a: 1 })).a.toFixed(); // compiler error now, that's good

现在这就完全按照你想要的方式工作了。TypeScript会看到函数输出拥有相同的元素,但没有额外属性。


但这也比你需要的更复杂一些。在 copyArr() 里面,我并不使用 t 可能具备的任何额外属性,我只关心元素类型。真正让我关心的不是 T,而是 T[number]。也就是说我应该写成:

function copyArr<E extends unknown>(t: Array<E>) {
  return t.slice();
}
const s = copyArr(["a"]).map(x => x.toUpperCase()); // yay
const oops = copyArr(Object.assign([], { a: 1 })).a.toFixed(); // error now, that's good

其中现在的泛型类型 E 只是元素类型,t 的类型是 Array<E>。现在 t.slice() 自然会返回另一个 E[],一切都按预期工作。对于zod,这意味着不是 T extends z.ZodObjectschema 的类型为 T,而是应该是 S extends z.core.$ZodShapeschema,类型为 z.ZodObject<S>,如前文所述。

Playground链接到代码

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章