Под капотом — движок
Происходит две вещи: на сборке из типа выводится схема, а в рантайме по данным работает валидатор. И то, и другое спроектировано так, чтобы исчезнуть — ничего не писать и ничего не чувствовать.
Схемы берутся из разрешённых типов
Плагин никогда не разбирает синтаксис типа. Он спрашивает у компилятора
TypeScript (через ts-morph) разрешённый тип в каждой точке. Поэтому всё, что
компилятор умеет свести к конкретной форме, ему по зубам — дженерики, indexed
access, условные типы, mapped-типы, утилити-типы (Partial, Pick, Omit,
Required). К моменту, когда t12n видит тип, это уже плоская структура.
type EventMap = {
user: { id: string; name: string; role: 'admin' | 'user' }
payment: { amount: number; currency: string; timestamp: Date }
}
type EventData<T extends keyof EventMap> = EventMap[T]
// условный + indexed тип на границе:
const p: EventData<'payment'> = JSON.parse(raw)
t12n сперва разрешает EventData<'payment'>, затем выводит схему:
{ amount: number, currency: string, timestamp: Date (instanceof) }
Никакого спец-случая для условного/indexed типа — компилятор свернул его до
того, как t12n заглянул. Mapped-типы, вложенный index access
(System[K]['config']), T | null, массивы и enum’ы получаются точно так же.
Встроенные классы (Date, Map, Set, RegExp, типизированные массивы)
проверяются через instanceof; index signature ({ [k: string]: T })
становится record; template-literal тип валидируется как строка.
Двухуровневый рантайм
Для прод-сборок плагин компилирует каждый тип в отдельную функцию-валидатор и зашивает её в бандл — в рантайме схема не интерпретируется:
// сгенерировано один раз на тип, поднято в модуль
const __v0 = (v) => {
if (v === null || typeof v !== 'object') return fail('', 'object', v)
if (typeof v.amount !== 'number') return fail('amount', 'number', v.amount)
if (typeof v.currency !== 'string') return fail('currency', 'string', v.currency)
if (!(v.timestamp instanceof Date)) return fail('timestamp', 'Date', v.timestamp)
return { amount: v.amount, currency: v.currency, timestamp: v.timestamp }
}
Линейный код со статическим доступом к полям — ровно то, что вы написали бы
руками. Рекурсивные типы и live-Proxy-guard откатываются на компактный
интерпретатор дерева; всё остальное идёт компилируемым путём. В рантайме нет
eval / new Function, поэтому работает и под строгим Content-Security-Policy.
Это практически бесплатно
Сгенерированный валидатор никогда не приводит типы и ничего не аллоцирует для данных, которые уже совпадают: лишние ключи срезаются по copy-on-write, так что чистый объект возвращается как есть. В бенчмарке из репозитория (массив из 100 вложенных объектов):
| ops/s | относительно | |
|---|---|---|
| вручную | ~775 000 | ~1.2× быстрее |
| t12n (компилируемый) | ~640 000 | базовая |
| Zod v4 | ~75 000 | ~8.5× медленнее |
Примерно в 8× быстрее Zod и в пределах ~20% от валидатора, написанного
руками под этот самый тип — потому что сгенерированный код по сути им и
является. Настолько, что проверка незаметна на фоне сетевого запроса или
JSON.parse, которые эти данные и породили. Проверить самому:
node bench/runtime.bench.mjs.