Под капотом — движок

Происходит две вещи: на сборке из типа выводится схема, а в рантайме по данным работает валидатор. И то, и другое спроектировано так, чтобы исчезнуть — ничего не писать и ничего не чувствовать.

Схемы берутся из разрешённых типов

Плагин никогда не разбирает синтаксис типа. Он спрашивает у компилятора 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.