Guide

Pure Functions

Extend RunTypes with pure inline helpers and your own formats and mock generators.
Pure functions are small, self-contained helpers that the compiler inlines into the generated code. A helper that reaches outside itself fails the build.

Registering Custom Format Patterns

Register a string pattern once with registerFormatPattern, then reference it from any number of TF.String types. Its optional mockSamples are the values the mock generator draws from. The build rejects a regex a crafted input could use to freeze the validator (see Unsafe Regex Patterns); set unsafePattern: true for the rare pattern that check reads wrongly.

import * as TF from '@mionjs/run-types/formats';
import * as RT from '@mionjs/run-types/builders';
import {createValidateFn, registerFormatPattern} from '@mionjs/run-types';

// a sample that does not match the regex throws at registration
const slug = registerFormatPattern({
  source: '^[a-z0-9]+(?:-[a-z0-9]+)*$',
  mockSamples: ['my-post', 'hello-world-2'],
  message: 'must be a kebab-case slug',
});

// no samples: each build draws new ones from the regex, unless a literal createMockDataFn seed pins them
const sku = registerFormatPattern({source: '^[A-Z]{3}-[0-9]{4}$'});

// `(\w+\s?)*` fails the freeze check; unsafePattern says you checked it yourself
const wordRun = registerFormatPattern({
  source: '^(\\w+\\s?)*$',
  mockSamples: ['one two'],
  unsafePattern: true,
});

// validation and mocks both use the pattern
type Slug = TF.String<{pattern: typeof slug}>;
type Sku = TF.String<{pattern: typeof sku}>;
type WordRun = TF.String<{pattern: typeof wordRun}>;

// a builder schema takes the pattern value itself, no typeof
const Product = RT.object({sku: TF.string({pattern: sku})});

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

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

export {slug, sku, wordRun, isPost, Product};
export type {Slug, Sku, WordRun, Post};

Registering Custom Mock Functions

Register a mock function for a RunTypeKind to change what the mock generator returns for that kind. It gets the format annotation, so you can branch on its name or params.

import {RunTypeKind, type FormatAnnotation} from '@mionjs/run-types';
import {registerMockingFunction} from '@mionjs/run-types/mocking';

// from now on createMockDataFn uses this for every mocked string format
registerMockingFunction(RunTypeKind.string, (annotation: FormatAnnotation) => {
  if (annotation.name === 'email') return '[email protected]'; // friendlier than a random string
  return undefined; // defer to the built-in mock for everything else
});

export {};

Registering a Pure Function

Register a pure helper that needs one-time setup (a compiled regex, a lookup table) or composes another pure fn with registerPureFnFactory(factory). The factory returns the real function.

import {registerPureFnFactory} from '@mionjs/run-types/runtime';

// You get back the helper's id: your package name plus a hash of the helper, like '@acme/text#pf_9Zt1bRm4cVaPqL'.
export const slugify = registerPureFnFactory(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, '');
  };
});

Registering a Pure Function Without Setup

When the helper needs neither setup nor another pure fn, registerPureFn(fn) takes the function itself.

import {registerPureFn} from '@mionjs/run-types/runtime';

export const double = registerPureFn((input: number): number => input * 2);

Pure Function Rules

A helper must be self-contained: no outer-scope captures, no this, no await or yield, and no dynamic import. Anything it needs goes inside the function. Break a rule and the build reports an error.

Standard built-ins are allowed: Math, JSON, ArrayBuffer, DataView, typed arrays like Uint8Array, TextEncoder, TextDecoder, btoa and atob. I/O and async are not, because the helper is inlined into synchronous code: no fetch or network, no timers, no process or globalThis, nothing awaited. So crypto.subtle.digest does not fit (hash over typed arrays instead), while a synchronous call like crypto.randomUUID is fine.

Write the helper inline, right where you register it: a name anything else could reach is rejected.

// Rejected: a name something else could reach
import {validate} from './validators';        // imported
export const validate = (v) => /* … */ true;  // exported
const validate = (v) => /* … */ true;         // a named local binding
registerValidator(validate);

// Fine: inline at the call site
registerValidator((v: unknown) => typeof v === 'string');

Reusing One Pure Function From Another

To call one pure function from another, import its id and pass it to the factory utilities. The id is a real value, so your editor completes it and your type checker follows a rename or a move.

import {registerPureFnFactory} from '@mionjs/run-types/runtime';
import {slugify} from './custom-pure-fn.js';

export const slugList = registerPureFnFactory(function (utl) {
  // recorded at build time: slugify ships with slugList, and a typo fails the build
  const toSlug = utl.getPureFn(slugify) as (input: string) => string;
  return function _slugList(values: string[]): string[] {
    return values.map((value) => toSlug(value));
  };
});

Wrapping the Registrar in Your Own API

A library can wrap the registrar in its own function, with no diagnostics and no configuration. The id belongs to the caller's package and hashes the caller's own helper, so a wrapper is invisible in the result.

import {type PureFunction, type InjectPureFnId} from '@mionjs/run-types';
import {registerPureFn} from '@mionjs/run-types/runtime';

export function registerAcmePureFn<F extends (...args: any[]) => any>(
  fn: PureFunction<F>,
  id?: InjectPureFnId<F> // the build injects the id at every call site of the wrapper
) {
  if (!id) throw new Error('mion plugin did not run');
  return registerPureFn(fn, id);
}

// a consumer passes only the function and gets back the id of its own binding
export const compiledUpper = registerAcmePureFn((s: string): string =>
  s.toUpperCase()
);

Publishing Pure Functions in a Package

A pure function registered in one package can be used from another, like any export. Build the package with a mion bundler plugin or with mion compile, and the build adds one directory next to the bundle, published with the rest of dist. With mion compile, put the gen dir inside the output dir, for example --gen-dir dist/.mion.

dist/
  index.js
  index.d.ts
  mion-pure-fns/
    index.json                     # maps every exported name to its id
    @acme/text/9Zt1bRm4cVaPqL.js   # one compiled file per pure function
    @acme/text/Q2xVn7eK0sLmYb.js
The directory is build-time input only, so it adds nothing to any bundle. Its files are the same compiled modules your own build uses.

Using Pure Functions From a Package

A consuming build compiles only the functions it uses from that directory into its own output, and registers each one before anything calls it. A package built without mion has no such directory, so importing one of its ids fails the build with purefn-package-not-built, which names the id and the package. Build that package with mion, or publish its TypeScript sources with its output, and the build reads those instead.

Looking Up a Pure Function at Runtime

usePureFn and the other tracked lookups need an id the build can read. When the id only exists at runtime, such as one from a request, use getPureFnByKey and hasPureFnByKey: they take a plain string, and the build does not check them. Keep usePureFn for ids known at build time.

import {getRTUtils} from '@mionjs/run-types';

// wireId came in over the network
if (getRTUtils().hasPureFnByKey(wireId)) {
  const fn = getRTUtils().getPureFnByKey(wireId);
  // dispatch on it
}

Sharing Compiled Functions Across Bundles

A validator or encoder the build generates in one bundle can run in another that never runs the compiler, such as a browser client; mion's router works this way. Send the entry's CompiledFnData, whose code field holds the function body as text. On the other side, restore it with buildFactoryFromCode and write it into the runtime cache with addToRTCache.

import {getRTUtils, buildFactoryFromCode, type CompiledFnData, type CompiledTypeFn} from '@mionjs/run-types';

// `wire` arrived over the network as plain data.
function ingest(wire: CompiledFnData) {
  const restored: CompiledTypeFn = {...wire, createRTFn: buildFactoryFromCode(wire.code!)};
  getRTUtils().addToRTCache(restored);
}

// later, on the client, look it up by its hash
const isUser = getRTUtils().getRTFn(rtFnHash);

Pure functions work the same way, with buildPureFnFactoryFromCode and addPureFn. Your framework decides which entries a payload needs and how to version the envelope.

Overriding Generated Functions

The overrideX helpers register your own pure function for one type, and every matching createX call returns it instead of the generated body. Use one for a hand-tuned encoder, a wire format that is not the plain shape of your type, or a one-off bug workaround.

import {overrideJsonEncoder, createJsonEncoderFn} from '@mionjs/run-types';

// declared once, near the type; the function must be pure
overrideJsonEncoder<User>((user) => `{"id":${user.id}}`);

// the call site does not change
const encode = createJsonEncoderFn<User>();

Every factory has an override twin (overrideValidate, overrideJsonEncoder, overrideJsonDecoder, and so on). Your function takes the same arguments as the generated one: a validation-errors override receives the value, the path, and the errors list.

You can register one override per type and function; a second one, with any body, is a build error. Decoders use validation to tell union branches apart, so a looser validator loosens those decoders too, and the build warns you.

Limiting an Override to One Type

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. Give the type a brand when you mean one specific type.

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

// Only UserId, not every string.
overrideValidate<UserId>((v): v is UserId => typeof v === 'string' && v.startsWith('u_'));

Overrides in a Published Package

A package built with mion ships its overrides with its pure functions. Your project uses them for that package's types, under the ids the package's build gave them. Overriding one of those types again is a build error.

A package built without mion ships no overrides, so its types use the generated functions.
Copyright © 2026