We fixedEnhanced TypeScriptAnd the reflection gap

TypeScript decided it is "Just a Linter" and erase your types.
We respectfully put them back in the runtime in a way that's reliable and makes sense.

Two ways to describe a shape, One source of truth.

Write a plain TypeScript type (fastest, zero ceremony) or reach for the RT.* schema builders if you like the Zod / TypeBox feel. Both compile to the exact same validator — pick whichever you fancy, mix them in the same file.

import {createValidate} from 'ts-runtypes';

// Your TypeScript type is the single source of truth — nothing else to write.
type User = {
  id: number;
  name: string;
  email: string;
  roles: ('admin' | 'user')[];
};

// A specialized validator, generated from the type at build time.
const isUser = createValidate<User>();

isUser({id: 1, name: 'Ada', email: 'ada@example.com', roles: ['admin']}); // true
isUser({id: '1', name: 'Ada'}); // false
import * as TF from 'ts-runtypes/formats';
import {createValidate, type Static} from 'ts-runtypes';
import * as RT from 'ts-runtypes/schema';

// Prefer schemas? Describe the same shape with the RT.* builders (Zod / TypeBox style).
const userSchema = RT.object({
  id: TF.number(),
  name: TF.string(),
  email: TF.email(),
  roles: RT.array(RT.union([RT.literal('admin'), RT.literal('user')])),
});

// Same validator, same result — your call.
const isUser = createValidate(userSchema);

// Recover the TypeScript type from the schema whenever you need it.
type User = Static<typeof userSchema>;

Formats baked into your types

TypeFormats®

Ensure type safety with formats like:
email, uuidv4, ipv4, int32, positive and more.

The validator checks its exact shape, not just its kind. No regex to wire up, no separate schema to keep in sync.

Temporal Support

Full TC39 Temporal — PlainDate, ZonedDateTime, Duration… validated and serialized like any built-in.

import type * as TF from 'ts-runtypes/formats';
import {createValidate} from 'ts-runtypes';

// A format brands a string or number — the validator checks its exact
// shape, not just "is it a string".
type Account = {
  id: TF.UUIDv4;
  email: TF.Email;
  ip: TF.IPv4;
  logins: TF.PositiveInt;
};

const isAccount = createValidate<Account>();
isAccount({id: 'nope', email: 'ada@x.com', ip: '10.0.0.1', logins: 3}); // false — id isn't a uuid
import * as TF from 'ts-runtypes/formats';
import {createValidate, type Static} from 'ts-runtypes';
import * as RT from 'ts-runtypes/schema';

// The same formats, schema-first — the RT.* builders.
const account = RT.object({
  id: TF.uuidv4(),
  email: TF.email(),
  ip: TF.ipv4(),
  logins: TF.positiveInt(),
});

// Recover the TypeScript type from the schema.
type Account = Static<typeof account>;

const isAccount = createValidate(account);

One object, Every function.

The whole toolbelt, in one box

Stop gluing five libraries together. RunTypes shares a single type graph across everything it generates, so the validator and the serializer always agree on what your type means.

One type in, every function out →

// One real-world type — the single source of truth for everything below.
type Order = {
  id: TF.UUIDv4;
  customer: {name: string; email: TF.Email};
  items: {sku: string; qty: number; price: number}[];
  total: number;
  placedAt: Date;
  status: 'pending' | 'paid' | 'shipped';
};

Validate

const isOrder = createValidate<Order>();
isOrder(order); // true

const orderErrors = createGetValidationErrors<Order>();
orderErrors({...order, total: 'free'}); // [{path: ['total'], expected: 'number'}]

JSON

const toJson = createJsonEncoder<Order>();
const fromJson = createJsonDecoder<Order>();

const wire = toJson(order); // Date -> string, ready for the network
const back = fromJson(wire); // string -> Date again, typed as DataOnly<Order>

Binary

const toBytes = createBinaryEncoder<Order>();
const fromBytes = createBinaryDecoder<Order>();

const bytes = toBytes(order); // a compact binary buffer — smaller than JSON
const order2 = fromBytes(bytes); // back to a typed object

Mock

const mockOrder = createMockType<Order>();
const fake = mockOrder(); // a valid, randomized Order for your tests
const orderSchema = createStandardSchema<Order>();

// a Standard Schema v1 object — hand it to any tool that speaks the spec
orderSchema['~standard'].validate(order); // {value: order}
orderSchema['~standard'].validate({}); // {issues: [{message, path}, …]}

Speaks Standard Schema

The same type becomes a Standard Schema, the shared ~standard contract that tRPC, TanStack Form and Router, Hono and many more accept directly. One call, no adapter to write.

One spec, every framework →

The reflection TypeScript never shipped

import {getRunType, RunTypeKind} from 'ts-runtypes';

// One real type — the single source of truth.
type Order = {
  id: string;
  total: number;
  items: {sku: string; qty: number}[];
};

// Recover the actual RunType node — the traversable type graph TypeScript erased.
const orderRT = getRunType<Order>();

// Walk it like any tree: its kind, property names, nested children…
console.log(orderRT.kind === RunTypeKind.objectLiteral); // true
console.log(orderRT.children?.map((prop) => prop.name)); // ['id', 'total', 'items']

Recover the type graph

Get back a traversable RunType node — the same graph the library walks internally: kind, property names, nested children, format annotations and more. Bring a type or infer it from a runtime value, then read it however you need — to drive codegen, build forms, or power your own tooling.


Reflection you can actually walk →

AI Agents meets Deterministic

AI-generated human-readable labels & errors

Friendly field labels and error messages for your forms and UI — written for people, kept in sync with your type.

AI-generated real-world mock data

Believable sample data — real names, emails, addresses — for your tests and demos, with every value valid for its field.

The compiler writes the code, your agent fills the blanks

Some values the compiler can't invent — a clear field label, a friendly error message, a believable sample name. So it does the hard part: it scaffolds a real, type-accurate source file, your agent fills in the blanks, and the compiler keeps it all in sync.

1The compiler scaffolds

From your type it writes a real source file — every field in place, correctly typed, with each blank marked.

2The AI agent fills the gaps

Guided by the type, the agent writes the labels, messages and sample values into the blanks.

3The compiler checks & keeps in sync

It checks every value against the type and updates the file as your type changes — your edits kept.

import type * as TF from 'ts-runtypes/formats';

// models/user.ts
export interface User {
  name: TF.String<{ minLength: 2; maxLength: 60 }>;
  age: TF.Number<{ min: 0; max: 120 }>;
  email: TF.Email;
}

Performance is nothing without control

Toe to Toe with the fastest

Our performance matches the fastest validators (AJV, TypeBox, Typia)
Even in their faster JIT mode, but without any JIT compilation cost.

Validation throughput — is-valid check (ops/sec, higher is better)

ts-runtypes40.6M
typia39.7M
typebox-Jit38.2M
ajv-Jit36.9M
zod*7.9M

* Zod has no fast is-valid path — it validates by parsing to errors, so its bar is the error-reporting result.

Tested to the highest standard

Every transform, cache shape and generated function is covered — on top of an extensive structured suite spanning validation, JSON, binary, mocks and reflection.

Tree-shaken to the bone

Ship only what you call

Caches are demand-driven and every entry is its own module, so bundlers split and tree-shake natively. A file that only reflects an id ships zero validation code — and the Vite plugin adds zero runtime dependencies.


Build-time, not run-time →

type Order = {
  id: string;
  name: number;
  email: string;
};

const isUser = createValidate<User>();
// shown as a function for clarity — the real emit is a positional
// tuple: faster to initialise, fewer bytes on the wire
export function __rt_a1b_Xk7(value) {
  return typeof value === "object" && value !== null &&
  typeof value.id === "number" &&
  typeof value.name === "string" &&
  typeof value.email === "string";
}

 

Copyright © 2026