The evolution of validation libraries

Why stop at validation?
Your validator already knows the exact shape of your data. RunTypes turns that same knowledge into validation, serialization, mocking and reflection, straight from your TypeScript types.

One Type, Multiple functionality.

The whole toolbelt, in one box

Stop gluing many 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, multiple compiled functions 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';
};

Validation

The job you hire a validator for, with nothing to write: your TypeScript type is the schema. At build time RunTypes compiles it into a plain standalone check, which is how it matches the fastest JIT validators with zero runtime compilation. See the benchmarks →

The same type is also 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.

Type guard and error list

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

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

How validation works →

Standard Schema

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}, …]}

One spec, every framework →

JSON roundtrip

Your type compiles into two functions, one that serializes to JSON and one that restores it, so Date, bigint, Temporal and almost any other type that describes data survive the round trip.

Json roundtrip transparently enables RPC function calls in JavaScript, and free devs from hand writing coerce and transform logic.

RunTypes

type Session = {
  user: string;
  expiresAt: Date;
  roles: Set<string>;
};

const toJson = createJsonEncoderFn<Session>();
const fromJson = createJsonDecoderFn<Session>();

const wire = toJson({user: 'ada', expiresAt: new Date(), roles: new Set(['admin'])})!;
const back = fromJson(wire);

const expiresAt: Date = back.expiresAt; // a real Date again
const roles: Set<string> = back.roles; // a real Set again, nothing to revive

Zod (hand maintained coercion and transform)

const sessionSchema = z.object({
  user: z.string(),
  // manually revive the Date
  expiresAt: z.coerce.date(),
  // manual Set revive
  roles: z.array(z.string()).transform((a) => new Set(a)), 
});

// encoding is also yours: JSON.stringify writes a Set as {}
const toJson = (session: Session) =>
  JSON.stringify({...session, roles: [...session.roles]});

const wire = toJson({user: 'ada', expiresAt: new Date(), roles: new Set(['admin'])});
const back = sessionSchema.parse(JSON.parse(wire));

// nothing checks toJson and sessionSchema agree:
// Devs must manually keep the two directions in sync

JSON that keeps your types →

Mocking that conforms to your types

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

Mock data from your types →

Binary serialization

const toBytes = createBinaryEncoderFn<Order>();
const fromBytes = createBinaryDecoderFn<Order>();

const bytes = toBytes(order); // a Uint8Array: the compact wire, smaller than JSON
const order2 = fromBytes(bytes); // back to a typed object

Compact bytes on the wire →

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

We support native TypeScript types (fastest, zero ceremony) or the RT.* type builders if you like the Zod / TypeBox feel. Both compile to the exact same validator, so pick whichever you fancy and mix them in the same file. And when you only take the type back out of a builder, the schema itself adds nothing to your bundle.

import type * as TF from '@ts-runtypes/core/formats';
import {createValidateFn} from '@ts-runtypes/core';

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

// A specialized validator, generated from the type at build time.
const isUser = createValidateFn<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/core/formats';
import {createValidateFn, type InferType} from '@ts-runtypes/core';
import * as RT from '@ts-runtypes/core/builders';

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

// Recover the TypeScript type, then generate from the type. Same validator,
// same result. Used this way the builder itself adds zero generated data.
type User = InferType<typeof userRunType>;
const isUser = createValidateFn<User>();

isUser({id: 1, name: 'Ada', email: 'ada@example.com', roles: ['admin']}); // true

Types and type builders, side by side →

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 and the rest), validated and serialized like any built-in.

Every format you can use →

import type * as TF from '@ts-runtypes/core/formats';
import {createValidateFn} from '@ts-runtypes/core';

// 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 = createValidateFn<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/core/formats';
import {createValidateFn, type InferType} from '@ts-runtypes/core';
import * as RT from '@ts-runtypes/core/builders';

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

// Recover the TypeScript type from the run-type.
type Account = InferType<typeof account>;

const isAccount = createValidateFn(account);

The reflection TypeScript never shipped

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

// 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 →

// Already have a value? Pass it straight in and its type is inferred.
const order: Order = {id: 'A-1', total: 42, items: [{sku: 'WIDGET', qty: 2}]};

// getRunType(order) returns the very same node as getRunType<Order>().
const orderRT = getRunType(order);
console.log(orderRT.children?.map((prop) => prop.name)); // ['id', 'total', 'items']

Infer types from a values

You don't have to write the type out. Hand getRunType any value and it reflects that value's static type, so getRunType(order) returns the same node as getRunType<Order>(). Reach for it when you already hold the data and just want its shape.

High performance compiled code

Every library says they are the fastest, we back it up!

Our performance matches and surpass the fastest validators (AJV, TypeBox, Typia) and we have the most comprehensive benchmark suite to back it up.

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

9,453front-end testsVitest (marker + plugin)
1,298Go testsgo test ./internal
Fuzzy TestingRandom inputs and randomly-generated types, checked against invariants, with every finding replayable from a seed.

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

Compile time code generation

Ship only what you use

Generated code is 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 = createValidateFn<User>();
import {__rt_a1b_Xk7} from './__runtypes/types/a1b_Xk7.js';

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

const isUser = createValidateFn<User>(__rt_a1b_Xk7);
// 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";
}

AI Agents meets Deterministic

Experimental

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, keeping your edits.

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

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

 

Copyright © 2026