Reflection
A stable id for any type
getRunTypeId hands you a short, stable id for any type. Bring the type yourself, or let it infer the type from a value you already have. Either way you get the same id back.
// packages/examples/src/guide/markers-reflection.ts
import {getRunTypeId} from 'ts-runtypes';
// Static form — you bring the type, you get its id. No value needed.
const stringId = getRunTypeId<string>(); // e.g. "Sq3kZ1"
const userId = getRunTypeId<{id: number; name: string}>();
// Reflection form — T is inferred from a value. The value is only read for
// its type; at runtime it's ignored, so nothing leaks into the output.
const order = {id: 1, total: 42};
const orderId = getRunTypeId(order);
// Same shape in, same id out — getRunTypeId<{id: number}>() and a value of
// that shape resolve to the exact same string.
export {stringId, userId, orderId};
The id is a fingerprint of the type's shape, so two types that look the same share one id. Use it as a cache key, a registry key, a discriminator, or whatever you need.
getRunTypeId throws rather than guess. The design behind it.InjectRunTypeId, the one you'll type
Every createX factory works because of one trick: a trailing parameter the build fills in with the type's id at each call site. The good news is that you can use the same trick yourself.
Add a trailing id?: InjectRunTypeId<T> to a generic function and it opts in. The build injects the id at every call site, so you never pass it by hand.
// packages/examples/src/guide/markers-wrap-helper.ts
import {getRTUtils, type InjectRunTypeId} from 'ts-runtypes';
// Wrap ts-runtypes into your OWN helper. Declare a trailing
// `id?: InjectRunTypeId<T>` parameter and the build fills it in at every
// call site — you never pass the id yourself.
function describe<T>(id?: InjectRunTypeId<T>): string {
// At runtime `id` is just the resolved hash string. Look the type up in
// the registry and do whatever your helper needs.
const runType = getRTUtils().getRunType(id!);
return runType ? `type #${id}` : 'unknown type';
}
// Call it like any generic function — no id argument in sight.
describe<{id: number; name: string}>();
describe<string[]>();
export {describe};
The call looks like any ordinary generic call, but inside, the injected id is just the type's hash, ready to look up or branch on.
T is concrete, at a real call site, not inside a generic body. To pass it through your own generic function, declare id?: InjectRunTypeId<T> and let the build fill it at each call site (exactly what the wrapper examples below do).Arguments read at compile time
Some factory arguments aren't ordinary runtime values, such as the ValidateOptions bag or the JSON encoder strategy. The build reads them to choose the exact specialized function it emits, so each one has to be a literal written at the call site. A variable won't do: the build can't see its value.
// packages/examples/src/guide/markers-comptime.ts
import {createValidate, createJsonEncoder} from 'ts-runtypes';
type Flag = {kind: 'on' | 'off'};
// These options are read by the BUILD, so they must be a literal written right
// at the call site — the build picks the specialized function from what it sees.
const isFlag = createValidate<Flag>({noLiterals: true});
const encode = createJsonEncoder<Flag>({strategy: 'direct'});
// A computed value is NOT a literal, so the build can't read it — this line
// fails compilation with a CTA diagnostic.
const looseAtNight = new Date().getHours() < 6;
createValidate<Flag>({noLiterals: looseAtNight});
export {isFlag, encode};
This rule is a marker, too. CompTimeArgs<T> brands a parameter as "must be a literal here", and CompTimeFnArgs<T> does the same for the literal that selects a variant (like the strategy above). The factories already declare them. You just pass a literal, and the build rejects anything else at compile time.
Wrapping your own helpers
The real payoff is composing the generated functions into higher-level helpers, say one call that parses JSON and validates it against your type in a single step.
// packages/examples/src/guide/markers-wrap-parse.ts
import {createValidate, type InjectRunTypeId} from 'ts-runtypes';
// A realistic wrapper: parse JSON and validate it against T in one call.
// The trailing `id?: InjectRunTypeId<T>` opts the helper into the toolchain.
function parseChecked<T>(raw: string, id?: InjectRunTypeId<T>): T {
const data = JSON.parse(raw) as T;
// Build a validator for T (createValidate is itself marker-driven).
const isValid = createValidate<T>();
if (!isValid(data)) throw new Error(`bad payload for type #${id}`);
return data;
}
type User = {id: number; name: string};
// One call site, fully typed. The build injects User's id behind the scenes.
const user = parseChecked<User>('{"id":1,"name":"Ada"}');
export {parseChecked, user};
One call site, fully typed, and the build wires the type's id through behind the scenes. Your callers never see a marker.
All Special Markers
You almost certainly won't type these (the createX factories use them for you), but here's the cast, so nothing's a mystery.
Factory injection
id?: InjectTypeFnArgs<T, Fn>
InjectRunTypeId's cousin for the factories. It injects a [typeId, fnId] pair so the build emits only the function you actually asked for. Every createX already declares it; you don't.
Compile-time literals
options: CompTimeArgs<T>
Brands a parameter as "must be a literal at the call site" (or a const of literals). It's how options bags get read at build time. 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 inline functions
fn: PureFunction<F>
Brands a function argument as "inline definition that passes the purity rules". Used for pure functions.
That's the whole family. One you'll type, and four that quietly do their job.