Guide

Validation

Type guards and error reports generated from your type at build time.

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: T is inferred from a value you already have.
  • Schema-first: pass an RT.* schema; T comes 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});
OptionWhat it does
noLiteralsLiteral checks degrade to their base type, so literal 'a' accepts any string and literal 2 accepts any finite number.
noIsArrayCheckSkips the leading Array.isArray() guard on array validators, for when you've already proven the value is an array upstream.
Pass the options as an object literal at the call site. It's read at build time, so a variable won't work because the build can't see its value.

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.

FactoryWhat it does
createHasUnknownKeysPredicate that returns true if the value carries any undeclared key.
createStripUnknownKeysDeletes undeclared keys in place, returns the same reference.
createUnknownKeyErrorsOne {path, expected: 'never'} per undeclared key, the same shape as createGetValidationErrors.
createUnknownKeysToUndefinedKeeps 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};

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.
Copyright © 2026