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 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.
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.
AI Agents meets Deterministic
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;
}
import type { FriendlyType } from 'ts-runtypes';
import type { User } from './user';
// scaffolded by `gen`: every field in place, each blank marked @todo
export const userFriendly: FriendlyType<User> = {
$label: '', // @todo
name: {
$label: '', // @todo
$errors: {
type: '', // @todo
minLength: '', // @todo
maxLength: '', // @todo
},
},
age: {
$label: '', // @todo
$errors: {
type: '', // @todo
min: '', // @todo
max: '', // @todo
},
},
email: { $label: '', $errors: { pattern: '' } }, // @todo
};
import type { FriendlyType } from 'ts-runtypes';
import type { User } from './user';
export const userFriendly: FriendlyType<User> = {
$label: 'User account',
name: {
$label: 'Full name',
$errors: {
type: '$[label] must be a valid name',
minLength: '$[label] needs at least $[val] characters',
maxLength: '$[label] allows at most $[val] characters',
},
},
age: {
$label: 'Age',
$errors: {
type: '$[label] must be a number',
min: '$[label] must be at least $[val]',
max: '$[label] must be no more than $[val]',
},
},
email: { $label: 'Email', $errors: { pattern: 'Enter a valid email address' } },
};
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.
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.
type Order = {
id: string;
name: number;
email: string;
};
const isUser = createValidate<User>();
import {__rt_a1b_Xk7} from 'virtual:rt/a1b_Xk7.js';
type Order = {
id: string;
name: number;
email: string;
};
const isUser = createValidate<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";
}