2. $constructor プロトタイプトレイトシステムと不変性
Zod v4.6.7 におけるすべてのスキーマ、チェック、およびエラークラスは、packages/zod/src/v4/core/core.ts で定義された独自のプロトタイプベース・トレイトファクトリ $constructor と、packages/zod/src/v4/core/util.ts のディスクリプタ/遅延メモ化ユーティリティの上に構築されています。深い ES6 class extends 階層を使用する代わりに、$constructor はスキーマの振る舞いをフラットでべき等なトレイトとして合成し、すべての内部メタデータを単一の列挙不可スロット _zod に隔離し、さらに Function.prototype[Symbol.hasInstance] を Set<string> ルックアップに置き換えることで、v4/core、v4/classic、v4/mini、およびデュアル CJS/ESM モジュール領域を跨いだ一貫した型判定を実現しています。
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line
from drawlib.shapes import rectangle
from drawlib.smartarts import CardList, Panel
from drawlib.styles import Styles
from drawlib.text import text
setup(width=176, height=112)
text(
(88, 107.5),
"$constructor プロトタイプトレイト合成と Internals アーキテクチャ",
style=Styles.DarkBold.patch(text_size=13.5),
)
text(
(88, 103.5),
"packages/zod/src/v4/core/core.ts によるマルチトレイト合成、_zod 内部状態の隔離、およびトレイト集合による instanceof 評価",
style=Styles.Muted.patch(text_size=10.0),
)
# ==============================================================================
# TOP LEFT: 1. $constructor Factory & _.init() Pipeline (x=5..86, y=49..99)
# ==============================================================================
p_init = Panel(
"1. $constructor(name, initializer, proto?, params?)",
style=Styles.PrimaryNeutral,
header_style=Styles.Primary,
text_style=Styles.WhiteBold.patch(text_size=10.5),
icon=phosphor.gear,
icon_style=Styles.White,
header_height=6.0,
)
p_init.draw(xy=(5, 49), width=81, height=50)
init_cards = CardList(
style=Styles.White,
text_style=Styles.DarkBold.patch(text_size=9.5),
description_style=Styles.Muted.patch(text_size=9.5),
badge_shape="none",
direction="vertical",
spacing=1.4,
)
init_cards.add(
"ステップ A: 列挙不可 _zod の割り当て (new Internals)",
description="共有 _zodDesc ({ enumerable: false }) で def, constr, traits を定義",
style=Styles.PrimaryFlat,
text_style=Styles.WhiteBold.patch(text_size=9.5),
description_style=Styles.White.patch(text_size=9.5),
)
init_cards.add(
"ステップ B: ダイヤモンド継承のべき等ガード (traits.has)",
description="traits.has(name) で重複初期化を防ぎ name 登録後に initializer 実行",
)
init_cards.add(
"ステップ C: WeakSet によるプロトタイプメンバー設定",
description="WeakSet で追跡しプロトタイプ毎に1回のみ installMembers を実行",
)
init_cards.add(
"ステップ D: deferred キュー実行と Post-Processor フック",
description="inst._zod.deferred を実行し globalConfig.postProcessor を呼び出し",
)
init_cards.draw(xy=(7.5, 51.0), width=76, height=40.0)
# ==============================================================================
# TOP RIGHT: 2. Multi-Trait Composition DAG (ZodEmail) (x=90..171, y=49..99)
# ==============================================================================
p_dag = Panel(
"2. マルチトレイト・ダイヤモンド合成 (ZodEmail)",
style=Styles.Neutral,
header_style=Styles.Secondary,
text_style=Styles.WhiteBold.patch(text_size=10.5),
icon=phosphor.git_merge,
icon_style=Styles.White,
header_height=6.0,
)
p_dag.draw(xy=(90, 49), width=81, height=50)
# Row 1 (Roots in v4/core)
rectangle((111.0, 86.5), width=35.0, height=8.0, style=Styles.White.patch(shape_r=1.5))
text((111.0, 88.2), "core.$ZodType.init", style=Styles.DarkBold.patch(text_size=9.5))
text((111.0, 84.8), "基底スキーマ (_zod.parse / run)", style=Styles.Muted.patch(text_size=9.5))
rectangle((150.0, 86.5), width=35.0, height=8.0, style=Styles.White.patch(shape_r=1.5))
text((150.0, 88.2), "core.$ZodCheck.init", style=Styles.DarkBold.patch(text_size=9.5))
text((150.0, 84.8), "基底チェック (_zod.check / bag)", style=Styles.Muted.patch(text_size=9.5))
# Row 2 (String & CheckStringFormat)
rectangle((111.0, 74.5), width=35.0, height=8.0, style=Styles.White.patch(shape_r=1.5))
text((111.0, 76.2), "core.$ZodString.init", style=Styles.DarkBold.patch(text_size=9.5))
text((111.0, 72.8), "typeof === 'string' 検証と bag", style=Styles.Muted.patch(text_size=9.5))
rectangle((150.0, 74.5), width=35.0, height=8.0, style=Styles.White.patch(shape_r=1.5))
text((150.0, 76.2), "core.$ZodCheckStringFormat", style=Styles.DarkBold.patch(text_size=9.5))
text((150.0, 72.8), "正規表現 / フォーマット検証", style=Styles.Muted.patch(text_size=9.5))
line((111.0, 82.5), (111.0, 78.5), arrow_head="->", style=Styles.PrimaryBold)
line((150.0, 82.5), (150.0, 78.5), arrow_head="->", style=Styles.SecondaryBold)
# Row 3 (Merged $ZodStringFormat -> $ZodEmail)
rectangle((130.5, 63.5), width=74.0, height=7.5, style=Styles.PrimaryFlat.patch(shape_r=1.5))
text((130.5, 65.2), "core.$ZodStringFormat.init --> ZodEmail.init", style=Styles.WhiteBold.patch(text_size=9.5))
text((130.5, 61.8), "スキーマとチェックを統合: z.email() と .check(z.email()) を両立!", style=Styles.White.patch(text_size=9.5))
line((111.0, 70.5), (120.0, 67.25), arrow_head="->", style=Styles.PrimaryBold)
line((150.0, 70.5), (141.0, 67.25), arrow_head="->", style=Styles.SecondaryBold)
# Row 4 (Classic vs Mini wrappers)
rectangle((111.0, 53.5), width=35.0, height=5.5, style=Styles.PrimaryNeutral.patch(shape_r=1.2))
text((111.0, 53.5), "classic.ZodEmail", style=Styles.DarkBold.patch(text_size=9.5))
rectangle((150.0, 53.5), width=35.0, height=5.5, style=Styles.PrimaryNeutral.patch(shape_r=1.2))
text((150.0, 53.5), "mini.ZodMiniEmail", style=Styles.DarkBold.patch(text_size=9.5))
line((120.0, 59.75), (111.0, 56.25), arrow_head="->", style=Styles.PrimaryBold)
line((141.0, 59.75), (150.0, 56.25), arrow_head="->", style=Styles.PrimaryBold)
# ==============================================================================
# BOTTOM: 3. Runtime Memory Layout & Trait-Set [Symbol.hasInstance] (x=5..171, y=5..44)
# ==============================================================================
p_mem = Panel(
"3. V8 高速プロパティ配置 (_zod Internals) と領域横断 instanceof ディスパッチ",
style=Styles.Neutral,
header_style=Styles.PrimaryFlat,
text_style=Styles.WhiteBold.patch(text_size=10.5),
icon=phosphor.cpu,
icon_style=Styles.White,
header_height=6.0,
)
p_mem.draw(xy=(5, 5), width=166, height=39)
mem_left = CardList(
style=Styles.White,
text_style=Styles.DarkBold.patch(text_size=9.5),
description_style=Styles.Muted.patch(text_size=9.5),
badge_shape="none",
direction="vertical",
spacing=1.4,
)
mem_left.add(
"inst._zod.def & inst._zod.constr (インオブジェクト・スロット)",
description="不変設定 { type: 'string' } と clone() 用のコンストラクタ参照",
style=Styles.PrimaryNeutral,
)
mem_left.add(
"inst._zod.run & inst._zod.parse (検証クロージャ)",
description="parse(payload, ctx) が型を検証し run(payload, ctx) がチェック実行",
)
mem_left.add(
"プロトタイプ連鎖: inst._zod --> zodProto (defineLazyInternal)",
description="共有遅延 getter (pattern, propValues) を zodProto に1回のみ定義",
)
mem_left.draw(xy=(7.5, 7.0), width=79, height=29.0)
mem_right = CardList(
style=Styles.White,
text_style=Styles.DarkBold.patch(text_size=9.5),
description_style=Styles.Muted.patch(text_size=9.5),
badge_shape="none",
direction="vertical",
spacing=1.4,
)
mem_right.add(
"inst._zod.traits = Set(7) { '$ZodType', 'ZodString', 'ZodCheck', ... }",
description="ダイヤモンド合成グラフ上の全初期化トレイト名を O(1) Set に記録",
style=Styles.AccentFlat,
text_style=Styles.WhiteBold.patch(text_size=9.5),
description_style=Styles.White.patch(text_size=9.5),
)
mem_right.add(
"inst instanceof core.$ZodString --> true",
description="カスタム [Symbol.hasInstance] が inst._zod.traits.has(name) を判定",
style=Styles.PrimaryNeutral,
)
mem_right.add(
"inst instanceof core.$ZodCheck --> true",
description="スキーマ階層とチェック階層の両方で CJS/ESM 領域を跨いで true!",
style=Styles.PrimaryNeutral,
)
mem_right.draw(xy=(89.5, 7.0), width=79, height=29.0)
save()
1. なぜ Zod v4 は ES6 クラスを $constructor に置き換えたのか
JavaScript および TypeScript の標準的な ES6 class B extends A 継承は、厳格な単一継承プロトタイプチェーンを強制します。Zod v4 のようなバリデーションライブラリでは、単一継承は以下の3つの重要な場面で破綻します。
- デュアルスキン・ファサード (
v4/classicvsv4/mini):v4/coreのすべてのスキーマ型($ZodString、$ZodObject、$ZodUnionなど)は、v4/classicのZodType(.min()、.max()、.optional()、.parse()等を付与)とv4/miniのZodMiniType(.parse()、.check()、.clone()のみを付与)の両方でラップされる必要があります。ES6 クラスでは、$ZodStringがすでに$ZodTypeを継承している場合、ZodStringがZodTypeと$ZodStringの両方を同時に継承することはできません。 - スキーマとチェックの二重役割を持つ型 (
$ZodStringFormat): Zod v4 では、z.email()、z.uuid()、z.url()などの文字列フォーマット検証器は、単独のスキーマ(z.email().parse(val))であると同時に、合成可能なバリデーションチェック(z.string().check(z.email()))でもあります。これには、$ZodStringFormatが$ZodStringのスキーマライフサイクルと$ZodCheckStringFormatのチェックライフサイクルの両方を継承する必要があります。 - モジュール領域(Realm)を跨いだ
instanceofの脆弱性: モノレポ、サーバーレスバンドル、あるいは CommonJS と ESM が混在する依存グラフでは、クラスコンストラクタのコピーがメモリ上に2つ存在したり、zod/miniで生成されたスキーマに対してschema instanceof $ZodTypeを評価したりすると、標準のFunction.prototype[Symbol.hasInstance]が失敗します。
$constructor(packages/zod/src/v4/core/core.ts:L51-145)は、**トレイトの初期化(_.init)と型の所属判定(inst._zod.traits)**を JavaScript の単一プロトタイプチェーンから分離することで、これら3つの問題をすべて解決しています。
2. $constructor の内部構造 (packages/zod/src/v4/core/core.ts)
2.1 シグネチャと型レベルのトレイト推論
// packages/zod/src/v4/core/core.ts
export type $constructor<T extends ZodTrait, D = T["_zod"]["def"]> = {
new (def: D): T;
init(inst: T, def: D): asserts inst is T;
};
$constructor(name, initializer, proto?, params?) を呼び出すと、コンストラクタ関数 _ が返されます。これは new _(def) として具象インスタンスを生成できるだけでなく、サブトレイトが生成した既存インスタンスに対して _.init(inst, def) を呼び出してトレイトのロジックを合成することもできます。
2.2 zodProto と V8 Hidden Class(隠しクラス)の最適化 (Internals)
$constructor が呼ばれてトレイトが定義されるたびに、専用のプロトタイプオブジェクト zodProto = {} と内部コンストラクタ Internals が生成されます。
// packages/zod/src/v4/core/core.ts
const zodProto = {};
function Internals(this: any, def: any) {
this.def = def;
this.constr = _;
this.traits = new Set();
}
Internals.prototype = zodProto;
なぜ core.ts はオブジェクトリテラル { def, constr: _, traits: new Set() } ではなく、new (Internals as any)(def) を使って inst._zod を割り当てるのでしょうか?
- インオブジェクト・プロパティの事前確保: V8 は
Internalsに対して専用の Hidden Class(Map)を割り当て、def、constr、traitsのインオブジェクト・スロットを事前確保します。 - 遅延内部プロパティ用の共有プロトタイプ (
zodProto):util.defineLazyInternal(inst, key, compute)を使って_zod上に遅延計算プロパティ(正規表現チェックの_zod.patternや、オブジェクト/リテラルの_zod.propValuesなど)を定義する際、getter は個々のスキーマインスタンスごとにObject.definePropertyを呼ぶのではなく、zodProto(Object.getPrototypeOf(inst._zod))上に1回だけインストールされます。これによりインスタンスごとのディスクリプタ生成コストを排除し、V8 が_zodを辞書モード(Dictionary Mode)へ降格させるのを防ぎます。
2.3 init(inst, def) の4段階ライフサイクル
スキーマまたはチェックのインスタンスに対して init(inst, def) が実行されると、以下の4つの決定論的ステップが順番に処理されます。
- 列挙不可
_zodスロットのアタッチ:inst._zodがまだ存在しない場合(親トレイトが走る前の最外殻コンストラクタ呼び出し時)、core.tsはtry/finallyブロック内で共有ディスクリプタ_zodDesc = { value: undefined, enumerable: false }を使って_zodを定義し、直後に_zodDesc.value = undefinedにクリアすることで、グローバルなディスクリプタが最後に生成されたスキーマへの参照をメモリ上に保持し続けないようにします。 - べき等なトレイトガード:
ダイヤモンド型のトレイトグラフは頻繁に発生します(例えばif (inst._zod.traits.has(name)) { return; } inst._zod.traits.add(name);ZodStringは$ZodType.initを呼ぶZodType.initと、同じく$ZodType.initを呼ぶ$ZodString.initの両方を呼び出します)。inst._zod.traits.has(name)をチェックすることで、各トレイトのinitializerが1つのインスタンスにつき高々1回しか実行されないことを保証します。 - Initializer の実行:
initializer(inst, def)が実行され、親トレイトを先に合成(ParentTrait.init(inst, def))した上で、inst._zod.parseやinst._zod.checkを特化させます。 - WeakSet で保護されたプロトタイプメンバーのインストール:
各コンストラクタは
const initialized = new WeakSet<object>()を保持しています。特定のObject.getPrototypeOf(inst)を持つインスタンスが初めてinitを通過した際、installMembers(target, protoMembers)がすべての遅延/バインド済みプロパティディスクリプタをtargetにコピーし、プロトタイプ直下の関数を自動バインドします。同じコンストラクタの2つ目以降のインスタンスでは、プロトタイプ設定が完全にスキップされます(O(1)のWeakSet.hasチェックのみ)。
2.4 高速な Error サブクラス化 ($ZodRealError 向けの newError)
$constructor が params: { Parent: Error } 付きで呼び出された場合(packages/zod/src/v4/core/errors.ts の $ZodRealError で使用)、V8 で通常通り new Error() を呼ぶと完全なコールスタックトレースがキャプチャされ、バリデーション失敗パスで大きなオーバーヘッドとなります。core.ts は newError(BaseClass) によってスタックフレームのキャプチャをバイパスします。
// packages/zod/src/v4/core/core.ts
function newError(BaseClass: ErrorConstructor) {
const _limit = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
const err = new BaseClass();
Error.stackTraceLimit = _limit;
return err;
}
2.5 領域横断の [Symbol.hasInstance] ディスパッチ
最後に、$constructor は _ 上にカスタムの Symbol.hasInstance トラップを定義します。
Object.defineProperty(_, Symbol.hasInstance, {
value: (inst: any) => {
if (params?.Parent && inst instanceof params.Parent) return true;
return inst?._zod?.traits?.has(name);
},
});
inst?._zod?.traits はトレイト名("$ZodType"、"$ZodString"、"$ZodCheck"、"$ZodEmail" など)の単純な Set<string> であるため、schema instanceof $ZodType と schema instanceof $ZodCheck はどちらも O(1) 時間で true と評価されます。これは v4/classic と v4/mini の間や、バンドラ内で重複したモジュールインスタンス間でも確実に機能します。
3. Copy-on-Write 不変性と循環安全な遅延メモ化 (v4/core/util.ts)
Zod v4 のすべてのスキーマメソッドは厳密に**不変(Immutable)**です。.min(3)、.optional()、.check(...) をチェーンしても、既存スキーマの _zod.def.checks 配列がインプレースで変更されることは決してありません。代わりに util.clone() によって軽量なクローンを生成し、高コストなメタデータ計算は循環参照に安全な遅延 getter(defineLazy および defineLazyInternal)によって初回パース時まで遅延されます。
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.smartarts import CardList, Panel
from drawlib.styles import Styles
from drawlib.text import text
setup(width=176, height=96)
text(
(88, 91.5),
"Copy-on-Write 不変性 (clone) と循環安全な遅延メモ化 (util.ts)",
style=Styles.DarkBold.patch(text_size=13.5),
)
text(
(88, 87.5),
"packages/zod/src/v4/core/util.ts による不変スキーマチェーン、再帰サイクルの遮断、およびプロトタイプディスクリプタ束縛",
style=Styles.Muted.patch(text_size=10.0),
)
# ==============================================================================
# PANEL 1 (LEFT): Copy-on-Write clone(inst, def) (x=5..58, y=6..83)
# ==============================================================================
p_clone = Panel(
"1. Copy-on-Write clone(inst, def)",
style=Styles.PrimaryNeutral,
header_style=Styles.Primary,
text_style=Styles.WhiteBold.patch(text_size=10.5),
icon=phosphor.copy,
icon_style=Styles.White,
header_height=6.0,
)
p_clone.draw(xy=(5, 6), width=53, height=77)
clone_cards = CardList(
style=Styles.White,
text_style=Styles.DarkBold.patch(text_size=9.5),
description_style=Styles.Muted.patch(text_size=9.5),
badge_shape="none",
direction="vertical",
spacing=1.6,
)
clone_cards.add(
"基底スキーマ: s1 = z.string()",
description="s1._zod.constr = ZodString; checks: []",
)
clone_cards.add(
"s1.min(3) --> s1.check(minLength(3))",
description="clone(s1, { ...def, checks: [c1] }) を実行",
style=Styles.PrimaryFlat,
text_style=Styles.WhiteBold.patch(text_size=9.5),
description_style=Styles.White.patch(text_size=9.5),
)
clone_cards.add(
"複製: s2 = new s1._zod.constr(newDef)",
description="s2._zod.parent = s1 を記録し _zod.run 配線",
)
clone_cards.add(
"インプレース変更ゼロの不変保証",
description="s1 は不変のまま _zod.run === parse を維持",
style=Styles.PrimaryNeutral,
)
clone_cards.draw(xy=(7.5, 8.5), width=48, height=66.5)
# ==============================================================================
# PANEL 2 (CENTER): Cycle-Safe Lazy Memoization (x=61.5..114.5, y=6..83)
# ==============================================================================
p_lazy = Panel(
"2. 循環安全な遅延評価 (Lazy)",
style=Styles.Neutral,
header_style=Styles.Secondary,
text_style=Styles.WhiteBold.patch(text_size=10.5),
icon=phosphor.arrows_clockwise,
icon_style=Styles.White,
header_height=6.0,
)
p_lazy.draw(xy=(61.5, 6), width=53, height=77)
lazy_cards = CardList(
style=Styles.White,
text_style=Styles.DarkBold.patch(text_size=9.5),
description_style=Styles.Muted.patch(text_size=9.5),
badge_shape="none",
direction="vertical",
spacing=1.6,
)
lazy_cards.add(
"defineLazy(object, key, getter)",
description="z.lazy() の getter、~standard、shape で使用",
style=Styles.PrimaryNeutral,
)
lazy_cards.add(
"EVALUATING センチネルによる循環遮断",
description="評価中の再突入時は即座に undefined を返却",
style=Styles.AccentFlat,
text_style=Styles.WhiteBold.patch(text_size=9.5),
description_style=Styles.White.patch(text_size=9.5),
)
lazy_cards.add(
"defineLazyInternal(inst, key, fn)",
description="zodProto に1回のみ定義 (propValues 等)",
style=Styles.PrimaryNeutral,
)
lazy_cards.add(
"一時 breaker と部分計算結果の破棄",
description="再帰発生時は部分結果を破棄し最上位のみ保持",
)
lazy_cards.draw(xy=(64.0, 8.5), width=48, height=66.5)
# ==============================================================================
# PANEL 3 (RIGHT): Prototype Descriptor Table (x=118..171, y=6..83)
# ==============================================================================
p_desc = Panel(
"3. プロトタイプ記述子 DSL",
style=Styles.PrimaryNeutral,
header_style=Styles.PrimaryFlat,
text_style=Styles.WhiteBold.patch(text_size=10.5),
icon=phosphor.stack,
icon_style=Styles.White,
header_height=6.0,
)
p_desc.draw(xy=(118, 6), width=53, height=77)
desc_cards = CardList(
style=Styles.White,
text_style=Styles.DarkBold.patch(text_size=9.5),
description_style=Styles.Muted.patch(text_size=9.5),
badge_shape="none",
direction="vertical",
spacing=1.6,
)
desc_cards.add(
"defineBound(proto, key, fn)",
description="初回参照時に fn.bind(this) をインスタンス束縛",
style=Styles.PrimaryFlat,
text_style=Styles.WhiteBold.patch(text_size=9.5),
description_style=Styles.White.patch(text_size=9.5),
)
desc_cards.add(
"derived(computes, table)",
description="初回参照時に ShapeGetter.raw で shape 凍結",
)
desc_cards.add(
"own(defineFn) & hide(defineFn)",
description="enumerable: true と false を宣言的に制御",
)
desc_cards.add(
"プロパティ参照時まで割り当てゼロ",
description="new ZodString(def) 時はクロージャ生成なし",
style=Styles.PrimaryNeutral,
)
desc_cards.draw(xy=(120.5, 8.5), width=48, height=66.5)
save()
4. util.ts の詳細解説: クローニング、遅延評価、プロトタイプディスクリプタ
4.1 clone(inst, def, params) (packages/zod/src/v4/core/util.ts:L645-649)
// packages/zod/src/v4/core/util.ts
export function clone<T extends schemas.$ZodType>(inst: T, def?: T["_zod"]["def"], params?: { parent: boolean }): T {
const cl = new inst._zod.constr(def ?? inst._zod.def);
if (!def || params?.parent) cl._zod.parent = inst;
return cl as any;
}
schema.check(minLength(3)) や schema.optional() を呼び出すと、Zod は clone(this, { ...this._zod.def, checks: [...(this._zod.def.checks ?? []), ...checks] }) を実行します。
inst._zod.constr は inst を生成した末端の具象コンストラクタ(例: v4/classic の ZodString や v4/mini の ZodMiniString)を指しているため、clone() は常に同じスキンの新しいインスタンスを生成し、$ZodType の deferred 初期化フックを再実行します。これにより、checks.length > 0 になった時点で cl._zod.run がチェック無しの高速パス(inst._zod.parse)からチェック実行ラッパーへと自動的に切り替わります。
4.2 defineLazy vs defineLazyInternal (util.ts:L338-361 & L1220-1258)
Zod v4 は、スキーマのライフサイクルの異なる階層に合わせて調整された2種類の遅延評価プリミティブを提供しています。
| プリミティブ | 定義対象オブジェクト | 循環参照の遮断メカニズム | v4/core における主な用途 |
|---|---|---|---|
defineLazy(object, key, getter) |
個別のオブジェクト (inst または def) |
クロージャ状態 value = EVALUATING を設定し、value === EVALUATING の間に再突入した場合は undefined を返す。 |
z.lazy() (def.getter)、Standard Schema (inst["~standard"])、$ZodUnion の options、$ZodError.issues。 |
defineLazyInternal(inst, key, compute) |
共有 zodProto (Object.getPrototypeOf(inst._zod)) |
this(inst._zod)上に一時的な breaker(get: () => undefined)を定義。再帰サイクルが breaker に到達した場合、不完全な結果をメモ化せず直後にプロパティを削除する。 |
_zod.propValues(判別共用体のルックアップ表)、_zod.pattern(統合正規表現)、_zod.values(enum/literal 集合)、_zod.iscanonical。 |
defineLazyInternal(util.ts:L1220-1258)の巧妙な設計に注目してください。相互再帰的なユニオンやオブジェクトが、コールスタックの上位で _zod.propValues を計算している最中に再び _zod.propValues を読み取ろうとした場合、内側の再帰読み取りは breaker 経由で undefined を受け取り、wasRecursive = true を記録します。内側のフレームが戻った際、defineLazyInternal は部分的な結果のキャッシュを拒否して Reflect.deleteProperty(this, key) を実行するため、完全な情報を持つ最上位の呼び出し元のみが最終的な値をメモ化できます。
4.3 宣言的プロトタイプディスクリプタ (members, defineBound, derived)
v4/classic/schemas.ts や v4/mini/schemas.ts では、ZodType や ZodObject 上の公開メソッドおよび getter は util.members(proto, table) を使って宣言されています。
// packages/zod/src/v4/core/schemas.ts ($ZodObject) の抜粋
const ShapeGetter = {
raw(inst: $ZodObject) {
return Object.freeze({ ...inst._zod.def.shape });
},
};
export const $ZodObject = $constructor("$ZodObject", (inst, def) => {
// ...
}, {
shape: util.own(util.derived(ShapeGetter, {
shape: (raw) => raw,
}).shape),
});
defineBound(proto, key, fn): 初回読み取り時にfn.bind(this)を返し、そのバインド済み関数をObject.definePropertyでthisに直接キャッシュする getter をproto上に定義します。これにより、z.string()を呼ぶたびにコンストラクタ内で40個以上のメソッドをバインドするメモリコストを回避しつつ、ユーザーがメソッドを分割代入(const { parse, safeParse } = UserSchema)してもthisコンテキストが失われないようになっています。derived(computes, table): 共有の補助計算(初回アクセス時にdef.shapeをクローンしてObject.freezeするShapeGetter.rawなど)を評価し、派生プロパティをスキーマインスタンス上にメモ化します。