Guide

Reflection

Reflect any type into a RunType node, get its stable id, and walk the graph.

Reflection gives you your TypeScript types at runtime: a short, stable id, or a RunType node you can walk. Validation, the JSON round trip and mocks read the same graph. Each type is one node, with a kind and links to the inner types it holds.

Getting a RunType Id

getRunTypeId returns a short, stable id for any type. Two types with the same shape share one id. Use it as a cache key, a registry key or a discriminator.

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

// static form: pass the type, no value needed
const stringId = getRunTypeId<string>(); // e.g. "Sq3kZ1"
const userId = getRunTypeId<{id: number; name: string}>();

// value form: T is inferred from `order`, which is ignored at runtime
const order = {id: 1, total: 42};
const orderId = getRunTypeId(order);

export {stringId, userId, orderId};
A compiler marker fills in the id at build time. With the Vite plugin off, getRunTypeId throws.

Getting a RunType Node

getRunType<T>() returns the node for a type.

// pass the type, or a value to infer it from
const orderRT = getRunType<Order>();

console.log(orderRT.kind === RunTypeKind.objectLiteral); // true: an object shape
console.log(orderRT.children?.map((prop) => prop.name)); // ['id', 'total', 'items']

// a property's `child` is its own type
const itemsRT = orderRT.children?.find((prop) => prop.name === 'items');
console.log(itemsRT?.child?.kind === RunTypeKind.array); // true: items is an array
console.log(itemsRT?.child?.child?.kind === RunTypeKind.objectLiteral); // true: of {sku, qty}

RunType Node Fields

Every node has a kind, one of the RunTypeKind values. The rest depends on that kind, but the edges that hold inner types are always one of these:

  • child is the single inner type, like an array's element, a promise's value, or a property's type.
  • children are the inner types of a container, like an object's properties, a union's members, or a tuple's slots.
  • parameters and return are a function's argument nodes and its return node.

Members also carry name and optional, literals and enums carry literal and values, and a branded TypeFormat carries formatAnnotation.

A formatAnnotation holds the canonical format name (uuid, email, dateTime) and optional params with that format's constraints. To map a format onto a database column or a UI label, look its name up in typeFormats: every built-in format name with the base kind it refines. Import typeFormats and the FormatName union of its names from @mionjs/run-types/runtime.

Names come from your source, including tuple labels and function parameter names. Two tuples with the same element types but different labels are two reflected types, each with its own labels.

Walking the RunType Graph

A full walk is a switch over kind that recurses through those edges. This printer renders any node back to a TypeScript-like string.

function describe(rt: RunType): string {
  switch (rt.kind as number) {
    // leaves: no inner types
    case RunTypeKind.string:
      return 'string';
    case RunTypeKind.number:
      return 'number';
    case RunTypeKind.boolean:
      return 'boolean';
    case RunTypeKind.bigint:
      return 'bigint';
    case RunTypeKind.symbol:
      return 'symbol';
    case RunTypeKind.null:
      return 'null';
    case RunTypeKind.undefined:
      return 'undefined';
    case RunTypeKind.void:
      return 'void';
    case RunTypeKind.never:
      return 'never';
    case RunTypeKind.any:
      return 'any';
    case RunTypeKind.unknown:
      return 'unknown';
    case RunTypeKind.object:
      return 'object';
    case RunTypeKind.regexp:
      return 'RegExp';
    case RunTypeKind.templateLiteral:
      return 'string'; // a `${...}` template
    case RunTypeKind.literal:
      return JSON.stringify(rt.literal); // the value itself
    case RunTypeKind.enum:
      return (rt.values as unknown[])
        .map((value) => JSON.stringify(value))
        .join(' | ');

    // single child: recurse into `child`
    case RunTypeKind.array:
      return `${describe(rt.child as RunType)}[]`;
    case RunTypeKind.promise:
      return `Promise<${describe(rt.child as RunType)}>`;
    case RunTypeKind.rest:
      return `...${describe(rt.child as RunType)}[]`;
    case RunTypeKind.tupleMember:
      return `${describe(rt.child as RunType)}${rt.optional ? '?' : ''}`;

    // containers: recurse over `children`
    case RunTypeKind.tuple:
      return `[${(rt.children as RunType[]).map(describe).join(', ')}]`;
    case RunTypeKind.union:
      return (rt.children as RunType[]).map(describe).join(' | ');
    case RunTypeKind.intersection:
      return (rt.children as RunType[]).map(describe).join(' & ');
    case RunTypeKind.objectLiteral:
      return `{ ${(rt.children as RunType[]).map(describe).join('; ')} }`;

    // members: `name`, `optional` and the member's own `child`
    case RunTypeKind.property:
    case RunTypeKind.propertySignature:
    case RunTypeKind.parameter:
      return `${rt.name as string}${rt.optional ? '?' : ''}: ${describe(rt.child as RunType)}`;

    // a key type and a value type
    case RunTypeKind.indexSignature:
      return `[key: ${describe(rt.index as RunType)}]: ${describe(rt.child as RunType)}`;

    // callables: recurse over `parameters` and `return`
    case RunTypeKind.function:
    case RunTypeKind.method:
    case RunTypeKind.methodSignature:
    case RunTypeKind.callSignature:
      return `(${(rt.parameters as RunType[]).map(describe).join(', ')}) => ${describe(rt.return as RunType)}`;

    // builtins (Date, Map, Set, Temporal) have no children: print the name
    case RunTypeKind.class:
      return rt.children
        ? `{ ${(rt.children as RunType[]).map(describe).join('; ')} }`
        : String(rt.typeName ?? 'object');

    // typeParameter, infer, ref, enumMember: rare in data shapes
    default:
      return `/* kind ${rt.kind} */`;
  }
}

const orderRT = getRunType<Order>();
console.log(describe(orderRT));
// { id: string; total: number; items: { sku: string; qty: number }[]; status: "open" | "shipped" | "cancelled" }
Builtin classes (Date, URL, Map, Set, Temporal) carry a subKind and a name instead of children, so treat them as leaves. Detect them by subKind (node.subKind === RunTypeSubKind.date), not by name: your own class could also be called Date. RunTypeSubKind comes from the package root, like RunTypeKind.

RunType Node Kinds

A node's kind is always one of these, grouped by how you walk it:

Atomic (leaves)    string  number  boolean  bigint  symbol  null  undefined  void
                   never  any  unknown  object  regexp  literal  templateLiteral  enum
Single child       array  promise  rest                      (read child)
Containers         tuple  union  intersection  objectLiteral  (read children)
Members            property  propertySignature  parameter  tupleMember  indexSignature
Callables          function  method  callSignature  methodSignature  (read parameters, return)
Class              class, covering Date / URL / Map / Set / Temporal or a plain shape (read subKind)
Type level (rare)  typeParameter  infer  enumMember  ref
Copyright © 2026