zodはTypeScript製の人気バリデーションライブラリです。今回選んだcompile.tsは「読み込むだけで効果を発揮する」副作用専用モジュールで、スキーマの実行部分を高速なコンパイル版にこっそり差し替える仕組みが55行に凝縮されています。ラッパー(shim)パターン、再入防止フラグ、設定によるフォールバックなど、実務でそのまま使えるパターンが詰まっているので選びました。
コード
// Side-effect-only: installs the post-processor on `globalConfig`. Listed in `package.json`'s `sideEffects`, and nothing else references it, so apps that never import it drop the compiler.
//
// Usage:
//
// import "zod/compile";
// import * as z from "zod";
// // every schema constructed below this is compiled on first parse
//
// Module evaluation order matters: schemas constructed in modules that evaluate before this import will not be compiled. Place this import in the app entry point, before any module that constructs schemas at top level.
//
// Failure handling: if the compiler refuses a schema (async refinement, unsupported feature, etc.) the shim permanently restores the runtime `_zod.run` for that schema. The schema continues to work via the regular runtime parser — no observable difference to the caller.
import { compile } from "./v4/core/compile.js";
import * as core from "./v4/core/index.js";
let compiling = false;
core.globalConfig.postProcessor = (inst: any) => {
if (compiling) return;
const originalRun = inst._zod?.run;
if (typeof originalRun !== "function") return;
const shim = (payload: any, ctx: any): any => {
// Bypass the fast path for any non-forward / non-sync / check-skipping call. The runtime owns those contracts.
if (ctx?.async || ctx?.direction === "backward" || ctx?.skipChecks) {
return originalRun(payload, ctx);
}
compiling = true;
try {
// Respect the jitless config: it exists precisely so CSP/no-eval environments never reach `new Function`. Global mode must not bypass it (explicit z.compile calls remain an explicit opt-in).
if (core.globalConfig.jitless) {
inst._zod.run = originalRun;
return originalRun(payload, ctx);
}
// Strict: the shim owns its own fallback below, and a non-strict compile would hand back `inst` — whose run is this shim — and reinstall it on itself.
const compiled = compile(inst, { strict: true });
// Only the run wrapper. Copying the compiled parse/safeParse closures would make their fallback re-enter this instance and run user callbacks a third time.
inst._zod.run = compiled._zod.run;
inst._zod.bag.fallbackRun = compiled._zod.bag.fallbackRun;
inst._zod.bag.validator = compiled._zod.bag.validator;
} catch {
// Permanent fallback for unsupported schemas.
inst._zod.run = originalRun;
} finally {
compiling = false;
}
return inst._zod.run(payload, ctx);
};
// Expose the pre-shim runtime so `compile()` invoked elsewhere can unwrap past the shim and capture the source-of-truth runtime. Without this, a user calling `z.compile(s)` after global mode is enabled would capture the shim itself, which would feed the wrapper into itself on fallback.
(shim as any).__originalRun = originalRun;
inst._zod.run = shim;
};
引用はリポジトリの実物と機械で照合しています。「// …略…」は省略した行です。
上から順に読む
import { compile } from "./v4/core/compile.js";
import * as core from "./v4/core/index.js";import文です。ソースファイルの拡張子は.tsですが、importのパスには.jsと書きます。これはNode.jsのESモジュール解決ルールに合わせたもので、TSはビルドされると.jsファイルになるため、実行時に読み込まれるファイル名を指しています。2行目の`* as core`は「core」という1つの名前の中に、そのモジュールがエクスポートしている全部をひとまとめにして詰め込むimportの書き方です。
let compiling = false;モジュールの一番外側(どの関数の中でもない場所)で宣言された変数です。関数の外にあるので、このファイルが一度読み込まれると、この後出てくる複数の関数呼び出しがすべてこの同じ変数を共有します。後の処理で「今まさに処理中かどうか」を示す旗(フラグ)として使われます。
core.globalConfig.postProcessor = (inst: any) => {オブジェクトのプロパティに関数を代入しています。`core.globalConfig`というオブジェクトが持つ`postProcessor`というプロパティ(フック用の差し込み口)に、無名のアロー関数を割り当てています。こうしておくと、zod側のどこかで「postProcessorがあれば呼び出す」という処理があり、スキーマが作られるたびにこの関数が実行される仕組みになります。`inst: any`は引数instの型をanyにする指定で、「型チェックはせず何でも受け取る」という意味です。
const originalRun = inst._zod?.run;
if (typeof originalRun !== "function") return;`?.`はオプショナルチェイニングという書き方です。`inst._zod`がnullやundefinedだった場合、その先の`.run`にアクセスしようとせずに全体がundefinedになります(エラーで止まりません)。次の行の`typeof`は値の型を文字列("function"や"undefined"など)で返す演算子で、それが関数でなければ`return`でこの関数の処理をここで打ち切ります(early return、早期リターン)。
const shim = (payload: any, ctx: any): any => {「shim」(差し替え役、詰め物)という名前の変数に、新しいアロー関数を代入しています。この関数は元の`run`関数と同じ引数(payloadとctx)を受け取り、内部で元の関数を呼び出しつつ前後に追加の処理を挟みます。既存の処理を壊さずに包んで機能を足す、いわゆる「ラッパー」や「デコレーター」と呼ばれる作り方です。
if (ctx?.async || ctx?.direction === "backward" || ctx?.skipChecks) {
return originalRun(payload, ctx);
}`||`はOR演算子で、左右どちらかがtrueなら全体がtrueになります。3つの条件のうち1つでも当てはまれば、最適化をせずに元の関数`originalRun`にそのまま処理を任せて`return`します。コメントにある通り、高速化の仕組みは「同期・順方向・チェック省略なし」の場合しか対応していないため、それ以外は安全に元の実装へ逃がしています。
compiling = true;
try {最初に説明した共有フラグ`compiling`をtrueにしてから、`try`ブロックに入ります。tryブロックは「この中で例外(エラー)が起きるかもしれない処理」を囲むための構文です。フラグを先に立てることで、この処理の途中でもし再びこの`postProcessor`が同じインスタンスに対して呼ばれても、「今コンパイル中だ」と判断できるようになります。
if (core.globalConfig.jitless) {
inst._zod.run = originalRun;
return originalRun(payload, ctx);
}設定オブジェクトのプロパティ`jitless`(evalが使えない環境向けの設定)を読んで分岐しています。trueなら、`inst._zod.run`というプロパティに元の関数を代入し直して元の状態に戻し、その場でも元の関数を呼び出します。オブジェクトが持つメソッド(ここでは`run`)を後から別の関数に差し替えることを「モンキーパッチ」と呼びます。
const compiled = compile(inst, { strict: true });別ファイルからimportした関数`compile`を呼び出しています。第1引数はインスタンス自身、第2引数は`{ strict: true }`という設定オブジェクトです。関数にオプションをたくさん渡したいとき、引数を並べるのではなく1つのオブジェクトにまとめて渡すのはTypeScript・JavaScriptでよく使われる書き方です。
inst._zod.run = compiled._zod.run;
inst._zod.bag.fallbackRun = compiled._zod.bag.fallbackRun;
inst._zod.bag.validator = compiled._zod.bag.validator;コンパイルによって作られた`compiled`というオブジェクトから3つのプロパティを取り出し、元の`inst`の同名プロパティに上書き代入しています。これにより、呼び出し側は同じ`inst`というオブジェクトを使い続けられるのに、中身の実装だけが高速版に置き換わります。
} catch {
// Permanent fallback for unsupported schemas.
inst._zod.run = originalRun;
} finally {
compiling = false;
}`catch`の後に変数名を書いていません。これはエラーオブジェクトの中身を使わない場合に使える省略記法です。コンパイルが失敗したら、`inst._zod.run`を元の関数に戻して安全側に倒します。`finally`ブロックはtryが成功しても失敗しても必ず実行される部分で、ここで`compiling`フラグを確実にfalseに戻しています。フラグを立てたら、結果に関わらずfinallyで必ず下ろす、というのは覚えておくと便利な後片付けのパターンです。
(shim as any).__originalRun = originalRun;`as any`はTypeScriptの型アサーションで、「この値をany型として扱ってコンパイラのチェックを通してほしい」という指示です。JavaScriptでは関数も一種のオブジェクトなので、`shim`という関数の上に`__originalRun`という独自のプロパティを後から追加できます。ここでは、他の場所からこのshimを見たときに元の実装を取り出せるように、目印として付けています。
inst._zod.run = shim;最後に`inst`の`run`プロパティを、これまで作ってきた`shim`関数に置き換えています。これで`postProcessor`フックの処理が完了し、以降このインスタンスの`run`が呼ばれるたびに、まずshimが動いて必要なら高速化されたコードにこっそり切り替わる、という仕組みが出来上がります。