Guide

Pure Functions

Extend RunTypes with pure inline helpers and your own formats and mock generators.
Pure functions are small, self-contained helpers that inline straight into the generated code, and the compiler validates that they're genuinely pure for you. A helper that reaches outside itself fails the build with a diagnostic, so an unsafe one never ships. (The exact rules are spelled out below.)

registerFormatPattern

Got a string shape you reuse, like a slug, a SKU, or an internal id? Register the regex once and reference it from any number of TF.String types.

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

// Register a reusable string pattern once. `mockSamples` are required —
// they double as canonical values the mock generator draws from, and each
// is checked against the regex at registration (a bad sample throws loudly).
const slug = registerFormatPattern({
  source: '^[a-z0-9]+(?:-[a-z0-9]+)*$',
  mockSamples: ['my-post', 'hello-world-2'],
  message: 'must be a kebab-case slug',
});

// Reference it by `typeof` in a TF.String. Build-time validation + mocks
// both pick it up.
type Slug = TF.String<{pattern: typeof slug}>;

type Post = {slug: Slug; title: string};

const isPost = createValidate<Post>();
isPost({slug: 'my-first-post', title: 'Hi'}); // true
isPost({slug: 'Not A Slug!', title: 'Hi'}); // false

export {slug, isPost};
export type {Slug, Post};

mockSamples are required and do double duty: the mock generator draws from them, and each one is checked against the regex at registration. A sample that doesn't match throws immediately, so a broken pattern fails loud and early.

registerMockingFunction

By default the mock generator invents something valid for each kind. Want mocked values to read a particular way? Register a mock fn for a RunTypeKind: return a value to override, or undefined to fall back to the default.

import {registerMockingFunction, RunTypeKind, type FormatAnnotation} from 'ts-runtypes';

// Want mock data to look a certain way for a kind? Register a mock fn for
// that ReflectionKind. Return `undefined` to fall back to the default mock.
// Here: make every mocked string format spit out a friendlier value.
registerMockingFunction(RunTypeKind.string, (annotation: FormatAnnotation) => {
  if (annotation.name === 'email') return 'someone@example.com';
  return undefined; // defer to the built-in mock for everything else
});

// From now on createMockType<T>() uses this when it mocks a string format.
// (createMockType itself is covered in the Mocking guide.)
export {};

You get the format annotation (its name and params), so you can branch: friendly emails for email, something else for the rest.

Pure functions

Register a pure helper with registerPureFnFactory("namespace::name", factory). The single "namespace::name" id keeps the helper easy to find, the factory returns the real function, and the build inlines it into the generated code.

import {registerPureFnFactory} from 'ts-runtypes';

// Pure functions are tiny, self-contained helpers the build can inline into
// the generated (JIT) code. The factory returns the real function; it must
// be self-contained — no outer-scope captures, no `this`, no await/yield.

export const slugify = registerPureFnFactory('app::slugify', function () {
  // Anything declared INSIDE the factory is fine — it ships with the helper.
  const NON_WORD = /[^a-z0-9]+/g;
  return function _slugify(input: string): string {
    return input.toLowerCase().replace(NON_WORD, '-').replace(/^-|-$/g, '');
  };
});

The rules are strict because the helper gets lifted out of its surroundings: it must be self-contained, with no outer-scope captures, no this, no await/yield, and no dynamic import. Anything it needs goes inside the factory (like the regex above). Break a rule and you get a build-time diagnostic.

Overriding compiler output

Sometimes you know a better function than anything the compiler can generate. A hand-tuned JSON encoder for a payload you send on every reply, a wire format that is not the plain shape of your type, or a quick workaround for a one-off bug. The overrideX helpers let you register your own pure function for one type, and every matching createX call returns it instead of the generated body.

import {overrideJsonEncoder, createJsonEncoder} from 'ts-runtypes';

// Declared once, near the type. The function must be pure, same rules as above.
overrideJsonEncoder<User>((user) => `{"id":${user.id}}`);

// Anywhere else, nothing changes at the call site:
const encode = createJsonEncoder<User>();

There is one override twin for every factory (overrideValidate, overrideJsonEncoder, overrideBinaryEncoder, and so on). Your function has to match the signature the family uses internally, so a validation-errors override receives the value, the path, and the errors list, exactly like the generated one.

An override applies to its type everywhere the type appears. If you override the encoder for a field type, every object that contains that field picks it up too. You can register exactly one override per type and function, so a second one (with any body) is a build error. Overriding validation is worth a second thought, because decoders use it to tell union branches apart, so a looser validator makes those decoders looser as well. The build warns you when that happens.

One thing to keep in mind: an override is matched by the shape of the type, not its name. So overrideValidate<string> applies to every string in the build, including string fields inside other objects. That is often what you want for a wire format, but when you mean one specific type, give it a brand so it has its own shape:

type UserId = string & {readonly __brand: 'UserId'};

// Only UserId, not every string.
overrideValidate<UserId>((v) => typeof v === 'string' && (v as string).startsWith('u_'));
Copyright © 2026