Open3
Zod v4のRecursive Objectでz.unionを使う方法
これまで
Zodで再帰的なオブジェクトスキーマ(Recursive Object)を定義しようとするとき、v3まではz.lazy()を使用しないとエラーが発生していた
import { z } from "zod";
const objA = z.object({
a: z.string(),
});
const objB = z.object({
b: z.string(),
});
const recursiveNg = z.object({
value: z.union([objA, objB, recursive]),
// ブロック スコープの変数 'recursive' が、宣言の前に使用されています。ts(2448)
});
ビルドが通るようにするにはこうする必要があった
import { z } from "zod";
const objA = z.object({
a: z.string(),
});
+ type ObjA = z.infer<typeof objA>;
const objB = z.object({
b: z.string(),
});
+ type ObjB = z.infer<typeof objB>;
+ type RecursiveOk = {
+ value: ObjA | ObjB | RecursiveOk;
+ };
- const recursiveNg = z.object({
- value: z.union([objA, objB, recursive]),
- });
+ const recursiveOk: z.ZodType<RecursiveOk> = z.object({
+ value: z.lazy(() => z.union([objA, objB, recursiveOk])),
+ });
個人的にはtype RecursiveOk
を定義しないと型推論として使えないのが非常に腹立たしかった
v4でどうなったのか(失敗例)
Introducind Zod 4によれば、 Recursive objects が簡単に書けるようになったらしい
const Category = z.object({
name: z.string(),
get subcategories(){
return z.array(Category)
}
});
type Category = z.infer<typeof Category>;
// { name: string; subcategories: Category[] }
先程のケースのを書き換えてみるとこうなる
import { z } from "zod/v4";
const objA = z.object({
a: z.string(),
});
const objB = z.object({
b: z.string(),
});
const recursiveV4Ng = z.object({
get value() {
return z.union([objA, objB, recursiveV4Ng]);
// マップされた型 '{ -readonly [P in keyof { readonly value: ZodUnion<readonly [ZodObject<{ a: ZodString; }, $strip>, ZodObject<{ b: ZodString; }, $strip>, ZodObject<..., $strip>]>; }]: { ...; }[P]; }' で、プロパティ 'value' の型によってそれ自体が循環参照されています。ts(2615)
},
});
マップされた型 '{ -readonly [P in keyof { readonly value: ZodUnion<readonly [ZodObject<{ a: ZodString; }, $strip>, ZodObject<{ b: ZodString; }, $strip>, ZodObject<..., $strip>]>; }]: { ...; }[P]; }' で、プロパティ 'value' の型によってそれ自体が循環参照されています。ts(2615)
どうやらz.union()はひと手間加える必要があるらしい
v4でどうなったのか(結論)
返り値の型を定義してあげればいいらしい
import { z } from "zod/v4";
const objA = z.object({
a: z.string(),
});
const objB = z.object({
b: z.string(),
});
const recursiveV4Ok = z.object({
- get value() {
+ get value(): z.ZodUnion<readonly [typeof objA, typeof objB, typeof recursiveV4Ok]> {
return z.union([objA, objB, recursiveV4Ok]);
},
});