Guide

Compiler Markers

The compile-time markers that tell RunTypes what to generate and inject.

A marker is a type you add to a function parameter, almost always the last one. You write the type, the build fills in the value, then the marker is erased. Every createX factory uses one, and you can use the same markers in your own helpers.

List of Compiler Markers

Factory injection

id?: InjectTypeFnArgs<T, Fn>

Validates the call site's compile-time arguments, then injects the generated functions you asked for. Every createX declares it, and so can your own wrappers.

Reflection id

id?: InjectRunTypeId<T>

Injects a short, stable id for T at each call site. The marker you are most likely to type, for wrapping your own helpers.

Compile-time literals

options: CompTimeArgs<T>

Brands a parameter as "must be a literal at the call site" (or a const of literals). Static check only.

Variant selection

options: CompTimeFnArgs<T>

Like CompTimeArgs, but the literal also selects which variant of a factory you get, such as the JSON encoder strategy. Static check only.

Pure functions

fn: PureFunction<F>

Brands a pure function argument that passes the purity rules. The direct form: the argument is the helper itself. See pure functions.

Pure function factories

fn: PureFunctionFactory<F>

The factory form of the above, for a helper that needs one-time setup or composes another pure fn. See pure functions.

Pure function id

id?: InjectPureFnId<F>

Injects a pure helper's id (its package plus a hash of its body), so a library can wrap the registrar. See pure functions.

Seven markers in total. The factories and registrars already declare them, so you type one only in a helper that wraps them.

Compile-Time Factory Arguments

Some factory arguments are read by the build, such as the ValidateOptions bag and the JSON encoder strategy. Write each one as a literal at the call site, or as a const of literals, which may live in another module.

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

type Flag = {kind: 'on' | 'off'};

// the build picks the generated function from these literals
const isFlag = createValidateFn<Flag>(undefined, {checkUnknowns: true});
const encode = createJsonEncoderFn<Flag>(undefined, {strategy: 'compact'});

// a computed value is not a literal, so the build fails with a marker-comptime-arg-* error
const strictAtNight = new Date().getHours() < 6;
createValidateFn<Flag>(undefined, {checkUnknowns: strictAtNight});

export {isFlag, encode};
Declare a shared options const with as const. Without it TypeScript widens {strategy: 'mutate'} to {strategy: string}, and the build rejects the widened value.

Injecting a Type Id into a Helper

Add a trailing id?: InjectRunTypeId<T> to a generic function and the build injects the type's id at every call site, so you never pass it by hand.

import {type InjectRunTypeId} from '@mionjs/run-types';
import {getRunType} from '@mionjs/run-types';

function describe<T>(id?: InjectRunTypeId<T>): string {
  // id is an opaque handle: forward it as the last argument to resolve T
  const runType = getRunType<T>(undefined, id);
  return `type #${runType.id} (kind ${runType.kind})`;
}

describe<{id: number; name: string}>();
describe<string[]>();

export {describe};
The marker only works where T is concrete: at a real call site, not inside a generic body.

Injecting a Type Id into a Class

A class constructor takes the same trailing marker.

import {type InjectRunTypeId} from '@mionjs/run-types';
import {getRunType} from '@mionjs/run-types';

class Described<T> {
  readonly text: string;
  constructor(value?: T, id?: InjectRunTypeId<T>) {
    const runType = getRunType<T>(undefined, id);
    this.text = `type #${runType.id} (kind ${runType.kind})`;
  }
}

new Described<{id: number; name: string}>(); // explicit T
const tags: string[] = ['a', 'b'];
new Described(tags); // inferred T, still gets the id

export {Described};

Wrapping Generated Functions

Compose the generated functions into your own helpers, such as one call that parses JSON and validates it against your type.

import {createValidateFn, type ValidateFn} from '@mionjs/run-types';

// take the validator as an argument: createValidateFn<T>() in this generic body is marker-in-generic-function
function parseChecked<T>(raw: string, isValid: ValidateFn<T>): T {
  const data: unknown = JSON.parse(raw);
  if (!isValid(data))
    throw new Error('payload does not match the expected type');
  return data as T;
}

type User = {id: number; name: string};

// User is concrete here, so the build can inject
const user = parseChecked('{"id":1,"name":"Ada"}', createValidateFn<User>());

export {parseChecked, user};

Requesting Several Functions in One Marker

One InjectTypeFnArgs marker can name several function families. A duplicate name is marker-duplicate-function-family. A name that matches no family is marker-unknown-function-family, and the error suggests the closest real name when there is one.

import {
  createGetValidationErrorsFn,
  createJsonDecoderFn,
  createJsonEncoderFn,
  type InjectTypeFnArgs,
} from '@mionjs/run-types';

type Handler = (...args: any[]) => unknown;

function route<H extends Handler>(
  handler: H,
  // the build injects one handle per family, in this order
  fns?: InjectTypeFnArgs<
    Parameters<H>,
    'validationErrors',
    'jsonDecoder',
    'jsonEncoder'
  >
) {
  const getErrors = createGetValidationErrorsFn(
    undefined,
    undefined,
    fns?.[0] as never
  );
  const decodeParams = createJsonDecoderFn(
    undefined,
    undefined,
    fns?.[1] as never
  );
  const encodeParams = createJsonEncoderFn(
    undefined,
    undefined,
    fns?.[2] as never
  );
  return {handler, getErrors, decodeParams, encodeParams};
}

// the build injects the three handles for this handler's parameters here
const greet = route((name: string, times: number) => name.repeat(times));

export {route, greet};

Turning a Handle into a Function

getRTFunction turns an injected handle into the callable function, with no factory. Pass it the same name you put in the marker. Every function can be named this way, and All Compiled Functions lists the names.

import {type InjectTypeFnArgs} from '@mionjs/run-types';
import {getRTFunction} from '@mionjs/run-types/runtime';

function jsonValueCodec<T>(
  fns?: InjectTypeFnArgs<T, 'prepareForJsonClone', 'restoreFromJsonClone'>
) {
  // the name repeated here is what gives getRTFunction the function's type
  const prepare = getRTFunction<'prepareForJsonClone'>(fns?.[0]);
  const restore = getRTFunction<'restoreFromJsonClone'>(fns?.[1]);
  return {prepare, restore};
}

type Message = {id: bigint; sentAt: Date; body: string};

// the build injects both handles for Message at this call site
const messageCodec = jsonValueCodec<Message>();

const message: Message = {
  id: 42n,
  sentAt: new Date('2020-01-02T03:04:05.000Z'),
  body: 'hi',
};

// your code owns the JSON.stringify and JSON.parse, so many values share one envelope
const wire = JSON.stringify(messageCodec.prepare(message));
const restored = messageCodec.restore(JSON.parse(wire));

export {jsonValueCodec, messageCodec, restored};

Function Hashes as Storage Keys

A framework that already holds a type's id can build the storage key for one of that type's functions itself. getFnHash returns the function half of that key, and you join it with the type id.

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

// the default validator's function id; every getFnHash id is stable across releases
const validateId = getFnHash('validate');

// options that change the generated function change its id too
const typeofValidateId = getFnHash('validate', {numberMode: 'typeof'});

// each JSON encoder strategy is its own function
const encodeMutateId = getFnHash('jsonEncoder', {strategy: 'mutate'});

// the type id is the value getRunTypeId returns
type User = {id: number; name: string};
const userTypeId = getRunTypeId<User>();
const userValidatorKey = `${validateId}_${userTypeId}`;

export {validateId, typeofValidateId, encodeMutateId, userValidatorKey};
Copyright © 2026