Validation
Every createX<T>() factory turns a type into a purpose-built function the build writes out for you, with no schema object and no runtime reflection. They all share one calling convention and one contract, so once you've seen one you've seen them all.
This page covers validation: the type guards, the error reports, and unknown-key handling. (Serialization, mocking and reflection get their own pages.)
Three ways to call any factory
Every factory takes your type three different ways. They all resolve to the same generated function, so use whichever fits the call site.
// 1. Type-first — you supply the type, no value needed.
const isPointA = createValidate<Point>();
// 2. Value-first — T is inferred from a value you already have.
const origin: Point = {x: 0, y: 0};
const isPointB = createValidate(origin);
// 3. Schema-first — pass an RT.* schema; T is inferred from the schema.
const pointSchema = RT.object({x: TF.number(), y: TF.number()});
const isPointC = createValidate(pointSchema);
- Type-first: you supply
<T>, no value needed. - Value-first:
Tis inferred from a value you already have. - Schema-first: pass an
RT.*schema;Tcomes from the schema.
The rest of these guide pages use whichever form reads best for the example, but every one of them accepts all three.
A fast yes/no with createValidate
createValidate<T>() gives you a type guard: pass a value, get a boolean, and TypeScript narrows the value inside the if.
// createValidate -> a type guard. Fast yes/no.
const isUser = createValidate<User>();
isUser({id: 1, name: 'Ada', roles: ['admin']}); // true
isUser({id: '1', name: 'Ada', roles: ['admin']}); // false — id is not a number
// It narrows too: inside the `if`, `data` is typed.
function handle(data: unknown) {
if (isUser(data)) data.roles; // ('admin' | 'user')[]
}
The full story with createGetValidationErrors
Same checks, but instead of a boolean you get an array of what failed. Each entry is {path, expected}: where it broke and what was expected. An empty array means valid; format failures add a format detail.
// createGetValidationErrors -> the same checks, but it tells you what broke.
const userErrors = createGetValidationErrors<User>();
userErrors({id: 1, name: 'Ada', roles: ['admin']}); // [] — all good
userErrors({id: '1', name: 42, roles: ['boss']});
// [
// {path: ['id'], expected: 'number'},
// {path: ['name'], expected: 'string'},
// {path: ['roles', 0], expected: "'admin' | 'user'"},
// ]
Tuning with ValidateOptions
Both validators take an options object as a literal at the call site. The build reads the literal and routes you to a specialized variant, so nothing is parsed at runtime.
// Pass an OBJECT LITERAL of options. The build reads the literal and routes
// the call to a specialized variant of the validator — nothing is read at runtime.
// noLiterals: a literal check degrades to its base type
// (here 'on' | 'off' becomes "any string").
const isFlagLoose = createValidate<Flag>({noLiterals: true});
// noIsArrayCheck: skip the leading Array.isArray() guard on array validators
// (handy when you've already proven the value is an array upstream).
const isFlagFast = createValidate<Flag>({noIsArrayCheck: true});
| Option | What it does |
|---|---|
noLiterals | Literal checks degrade to their base type, so literal 'a' accepts any string and literal 2 accepts any finite number. |
noIsArrayCheck | Skips the leading Array.isArray() guard on array validators, for when you've already proven the value is an array upstream. |
Unknown keys
Four tools for properties that aren't in your declared type. These check for extra keys. To check that the declared keys hold the right types, that's validation above.
| Factory | What it does |
|---|---|
createHasUnknownKeys | Predicate that returns true if the value carries any undeclared key. |
createStripUnknownKeys | Deletes undeclared keys in place, returns the same reference. |
createUnknownKeyErrors | One {path, expected: 'never'} per undeclared key, the same shape as createGetValidationErrors. |
createUnknownKeysToUndefined | Keeps the key but sets it to undefined instead of deleting it. |
import {createHasUnknownKeys} from 'ts-runtypes';
type User = {id: number; name: string};
// createHasUnknownKeys -> true if the value carries any key the type didn't declare.
const hasExtra = createHasUnknownKeys<User>();
hasExtra({id: 1, name: 'Ada'}); // false
hasExtra({id: 1, name: 'Ada', admin: true}); // true — `admin` isn't in User
export {hasExtra};
Strip, report and blank-out share the same shape. Pick the one that matches what downstream code expects:
import {createStripUnknownKeys} from 'ts-runtypes';
type User = {id: number; name: string};
// createStripUnknownKeys -> deletes undeclared keys in place, returns the same ref.
const strip = createStripUnknownKeys<User>();
const dirty = {id: 1, name: 'Ada', admin: true, token: 'secret'};
strip(dirty); // {id: 1, name: 'Ada'} — admin and token are gone
export {strip};
import {createUnknownKeyErrors} from 'ts-runtypes';
type User = {id: number; name: string};
// createUnknownKeyErrors -> one {path, expected: 'never'} entry per undeclared key.
const unknownKeyErrors = createUnknownKeyErrors<User>();
unknownKeyErrors({id: 1, name: 'Ada'}); // []
unknownKeyErrors({id: 1, name: 'Ada', admin: true});
// [{path: ['admin'], expected: 'never'}]
export {unknownKeyErrors};
import {createUnknownKeysToUndefined} from 'ts-runtypes';
type User = {id: number; name: string};
// createUnknownKeysToUndefined -> sets undeclared keys to undefined instead of deleting.
const blank = createUnknownKeysToUndefined<User>();
const value = {id: 1, name: 'Ada', admin: true};
blank(value); // {id: 1, name: 'Ada', admin: undefined} — key stays, value cleared
export {blank};
Standard Schema
createStandardSchema<T>() wraps the validators above into a Standard Schema object: the shared ~standard contract that tRPC, TanStack Form and Router, Hono, React Hook Form and many more accept directly. One call and your type works anywhere the spec is understood, with no per-library adapter to write.
// createStandardSchema -> a Standard Schema v1 object: a single `~standard`
// property that tRPC, TanStack Form/Router, Hono and others accept directly.
const userSchema = createStandardSchema<User>();
// Valid input comes back under `value`.
userSchema['~standard'].validate({id: 1, name: 'Ada', roles: ['admin']});
// {value: {id: 1, name: 'Ada', roles: ['admin']}}
// Invalid input comes back as a flat list of issues, each with a message + path.
userSchema['~standard'].validate({id: '1', name: 'Ada', roles: ['admin']});
// {issues: [{message: 'Expected number', path: ['id']}]}
The object exposes a single validate(value). It returns {value} when the value is valid, or {issues} when it is not, where each issue carries a message and a path locating the field that failed. Under the hood it runs the fast type guard first and only builds the issue list on a failure, so the valid path stays cheap.
It takes the same three call forms as every other factory (type-first, value-first, schema-first) and the same ValidateOptions.
validate is synchronous and never throws on invalid input. Instead it reports through issues. The result is typed as DataOnly<T> (the serializable projection covered above), so what a consumer reads back matches what the validator actually checks.