Zod の z.lazy() はどのような場面で使うのが正しいですか?
解説
正解は 「自分自身を含むような再帰的な構造のスキーマを定義するため」です。z.lazy() は、スキーマ定義の中で自分自身を参照したいときに使う特別なヘルパーです。「バリデーションの遅延実行」「起動速度の改善」「非同期検証」といった用途ではありません(非同期検証は .refine() と parseAsync() の役割です)。名前に引きずられて「遅延評価で高速化するやつ」と誤解しやすいので注意してください。なぜ再帰スキーマでz.lazyが必要なのかJavaScript では、変数は宣言される前には参照できません。ツリー構造のような再帰的なデータを普通に書こうとすると、この制約にぶつかります。// これはエラーになる const Category = z.object({ name: z.string(), children: z.array(Category), // Categoryはまだ初期化されていない });右辺を評価している時点で Category はまだ完成していないため、自分自身を参照できません。z.lazy() はスキーマの評価を「実際に使うとき」まで遅らせることでこの問題を回避します。type Category = { name: string; children: Category[] }; const Category: z.ZodType<Category> = z.lazy(() => z.object({ name: z.string(), children: z.array(Category), }) );コールバックの中身は parse() が呼ばれた瞬間に評価されるので、そのときには Category の変数はすでに定義済みになっている、という仕組みです。型推論が効かないので手動で型を渡す再帰スキーマでは Zod の z.infer による自動型推論が効きません。上の例のように、まず TypeScript の type で再帰型を定義し、z.ZodType<Category> のように型パラメータで明示する必要があります。これは Zod の制約というより、TypeScript の型システム自体が再帰推論を苦手としているためです。実務での使いどころネストしたコメント: コメントが返信を持ち、返信がさらに返信を持つような構造カテゴリツリー: 親カテゴリの下に子カテゴリが無限にぶら下がる構造ファイルシステム的な構造: フォルダの中にフォルダが入る逆に、再帰構造でないのに z.lazy() を使う理由はほぼありません。「遅延読み込みで最適化」的な発想で使うのは誤用です。