Your types vanish at runtime. t12n puts the check back.

Outside data slips in everywhere — an API response you assign to a typed variable, a function argument, JSON.parse, localStorage. t12n reads your type and drops a real check in right there, the moment the data lands. No schemas, no validate() calls — and in live mode it keeps watching the value for the rest of its life.

$ npm i t12n

v0.1 · beta ~3 KB/zero deps/~8× Zod

user.ts

type User = {
  id: string
  email: string
  age: number
}

// straight off the network — unchecked
const user: User =
  await fetch('/api/me').then(r => r.json())
// validator t12n compiled from User
function checkUser(v) {
  if (typeof v.id    !== 'string') fail('id')
  if (typeof v.email !== 'string') fail('email')
  if (typeof v.age   !== 'number') fail('age')
  return v
}

// the boundary is now guarded
const user = checkUser(
  await fetch('/api/me').then(r => r.json())
)
type Order = {
  id: string
  total: number
  status: 'paid' | 'pending'
}

// restored from localStorage — unchecked
const order: Order =
  JSON.parse(localStorage.cart)
// validator t12n compiled from Order
function checkOrder(v) {
  if (typeof v.id    !== 'string') fail('id')
  if (typeof v.total !== 'number') fail('total')
  if (v.status !== 'paid' &&
      v.status !== 'pending')      fail('status')
  return v
}

const order = checkOrder(
  JSON.parse(localStorage.cart)
)
type Message = {
  kind: string
  data: number[]
}

// arrives via postMessage — unchecked
const msg: Message = event.data
// validator t12n compiled from Message
function checkMessage(v) {
  if (typeof v.kind !== 'string') fail('kind')
  if (!Array.isArray(v.data))     fail('data')
  for (const n of v.data)
    if (typeof n !== 'number')    fail('data[]')
  return v
}

const msg = checkMessage(event.data)

01 the gap

A type is a promise the
runtime never checks.

without t12n

const user: User = await fetch('/me')
  .then(r => r.json())

// API actually sent { id: 1, email: null }

// …40 lines away, no trail back:
user.email.toLowerCase()
// 💥 Cannot read … of null

with t12n

const user: User = await fetch('/me')
  .then(r => r.json())

// the check is already here,
// generated from the User type.

// ✗ caught on this line — field,
//   expected type and what arrived.

02 how it works

You write the type.
The build writes the check.

1

Annotate a type

A normal type at a boundary, or the Check<T> marker where you want it explicit.

2

The plugin reads it

At build it resolves the real type via the TS checker — generics, unions, Date and all — into a schema.

3

A validator is compiled in

Each type becomes a dedicated, straight-line function baked into your bundle. No schema read at runtime.

03 everywhere data enters

Every boundary. Every framework.

Boundaries it watches

fetch().json()JSON.parse()localStoragepostMessageas any / unknownfunction paramsfunction returnsref / useState

Works with

React
Vue
Svelte
Solid
Next.js
Nuxt
Astro
VitewebpackRollupesbuildRspack

04 performance

Almost as fast as a check
you'd write by hand.

Static field access, no schema lookup, no coercion — nothing allocated when the data already matches. About 8× faster than Zod.

Hand-written the speed ceiling~775k/s
t12n compiled into the bundle~640k/s
Zod v4~75k/s
~3 KBgzipped runtime
0runtime dependencies
0values coerced — ever
CSPsafe — no eval at runtime

05 when it fails

When it breaks, you
know exactly where.

It fails on the line the data came in — and tells you the file and line, the field, the type you expected and what actually arrived. With configure() you decide what a failure does: throw in dev, quietly report in prod.

  • the file and line where the check fired
  • which boundary the value came from (fetch, JSON.parse…)
  • the path to the field, expected vs received

06 beyond the boundary

Validate once — or for the object's whole life.

A plain check runs once, at the boundary. Turn on live mode and t12n hands back a Proxy that keeps the type enforced for good — so a wrong write a hundred lines later is caught too.

Set up in about five minutes.

Add the plugin, write your types, and your typed boundaries start checking themselves.