RunTypes

RunTypes Overview

JIT compilation library that generates optimized validation, serialization, and mocking functions from TypeScript types.

@mionkit/run-types is a powerful JIT (Just-In-Time) compilation library that generates optimized validation, serialization, and mocking functions directly from TypeScript types. Unlike schema-based libraries like Zod or AJV, run-types leverages TypeScript's type system at compile time to generate highly efficient runtime code.

Key Features

  • Type-Safe Validation: Validate data against TypeScript types at runtime
  • JSON Serialization: Handle complex types (Date, BigInt, Map, Set) in JSON
  • Binary Serialization: Efficient binary encoding for performance-critical scenarios
  • Mock Data Generation: Generate valid test data from types
  • JIT Compilation: Functions are compiled on first use for optimal performance
RunTypes requires TypeScript type metadata at runtime. See Caveats for common pitfalls like type-only imports and missing type annotations. Use the ESLint rules to catch these issues.

Validation Functions

createIsTypeFn<T>()

Returns a function that checks if a value matches the type. Returns true or false.

import {createIsTypeFn} from '@mionkit/run-types';

interface User {
    name: string;
    age: number;
}

async function example() {
    const isUser = await createIsTypeFn<User>();

    isUser({name: 'John', age: 30}); // true
    isUser({name: 'John'}); // false (missing age)
    isUser({name: 'John', age: '30'}); // false (age is string)
}

createTypeErrorsFn<T>()

Returns a function that returns detailed error information when validation fails.

import {createTypeErrorsFn} from '@mionkit/run-types';

interface User {
    name: string;
    age: number;
}

async function example() {
    const getUserErrors = await createTypeErrorsFn<User>();

    const errors = getUserErrors({name: 123, age: 'invalid'});
    // Returns: [
    //   { path: ['name'], expected: 'string', actual: 'number' },
    //   { path: ['age'], expected: 'number', actual: 'string' }
    // ]
}

JSON Serialization Functions

createPrepareForJsonFn<T>()

Converts JavaScript values to JSON-compatible format. Handles special types like Date, BigInt, Map, Set.

import {createPrepareForJsonFn} from '@mionkit/run-types';

interface Event {
    name: string;
    timestamp: Date;
    metadata: Map<string, any>;
}

async function example() {
    const prepareEvent = await createPrepareForJsonFn<Event>();

    const event = {
        name: 'Click',
        timestamp: new Date('2025-01-15'),
        metadata: new Map([['source', 'web']]),
    };

    const jsonReady = prepareEvent(event);
    // { name: 'Click', timestamp: '2025-01-15T00:00:00.000Z', metadata: [['source', 'web']] }
    JSON.stringify(jsonReady); // Now works correctly!
}
prepareForJson mutates the original object for performance. This avoids creating unnecessary copies in request/response pipelines.
If you need to preserve the original object, use createStringifyJsonFn instead — it does not mutate the input.

createRestoreFromJsonFn<T>()

Restores JavaScript types from JSON-parsed data. The inverse of prepareForJson.

import {createRestoreFromJsonFn} from '@mionkit/run-types';

interface Event {
    name: string;
    timestamp: Date;
    metadata: Map<string, any>;
}

async function example() {
    const restoreEvent = await createRestoreFromJsonFn<Event>();

    const jsonString = '{"name":"Click","timestamp":"2025-01-15T00:00:00.000Z","metadata":[["source","web"]]}';
    const parsed = JSON.parse(jsonString);
    const event = restoreEvent(parsed);
    // event.timestamp is now a Date object
    // event.metadata is now a Map
}

createStringifyJsonFn<T>()

Directly parses types into JSON strings, does not modify the original object.

import {createStringifyJsonFn} from '@mionkit/run-types';

interface Event {
    name: string;
    timestamp: Date;
    metadata: Map<string, any>;
}

async function example() {
    const event = {
        name: 'Click',
        timestamp: new Date('2025-01-15'),
        metadata: new Map([['source', 'web']]),
    };

    const stringifyEvent = await createStringifyJsonFn<Event>();
    const jsonString = stringifyEvent(event);
    // Equivalent to: JSON.stringify(prepareForJson(event)) but faster
}
createStringifyJsonFn uses javascript to traverse the objects and convert them into JSON string. It does not use JSON.stringify directly!
This might result in a slightly slower performance than prepareForJson => JSON.stringify but does not mutate the input.

Binary Serialization Functions

For performance-critical scenarios, binary serialization provides compact encoding.

createToBinaryFn<T>()

Serializes a value to a compact binary format (Uint8Array).

async function toBinaryExample() {
    const toBinary = await createToBinaryFn<User>();
    const buffer = toBinary({name: 'John', age: 30});
    // Returns Uint8Array with optimized binary encoding
}

createFromBinaryFn<T>()

Deserializes a binary buffer back to the original type.

async function fromBinaryExample() {
    const fromBinary = await createFromBinaryFn<User>();
    const buffer = new Uint8Array(); // from previous example
    const user = fromBinary(buffer);
    // user is now { name: 'John', age: 30 }
}

Mock Data Generation

createMockTypeFn<T>()

Generates valid mock data for any type. Perfect for testing.

interface User {
    id: string;
    name: string;
    email: string;
    age: number;
    createdAt: Date;
}

async function basicMockExample() {
    const mockUser = await createMockTypeFn<User>();

    const user = mockUser();
    // {
    //   id: 'abc123xyz',
    //   name: 'mockString',
    //   email: 'test@example.com',
    //   age: 42,
    //   createdAt: Date('2025-01-15T12:00:00.000Z')
    // }
}

When using Type Formats, mock data respects format constraints:

import {StrEmail} from '@mionkit/type-formats/FormatsString';
import {NumPositiveInt} from '@mionkit/type-formats/FormatsNumber';

interface ValidatedUser {
    email: StrEmail;
    followersCount: NumPositiveInt;
}

async function formatsMockExample() {
    const mockValidatedUser = await createMockTypeFn<ValidatedUser>();
    const user = mockValidatedUser();
    // { email: 'user@example.com', followersCount: 150 }
}

Complete Example

import {
    createIsTypeFn,
    createTypeErrorsFn,
    createStringifyJsonFn,
    createRestoreFromJsonFn,
    createMockTypeFn,
} from '@mionkit/run-types';

interface BlogPost {
    id: string;
    title: string;
    content: string;
    author: {
        name: string;
        email: string;
    };
    tags: string[];
    publishedAt: Date;
    metadata: Map<string, any>;
}

async function completeExample() {
    // Create all needed functions
    const isPost = await createIsTypeFn<BlogPost>();
    const getErrors = await createTypeErrorsFn<BlogPost>();
    const stringify = await createStringifyJsonFn<BlogPost>();
    const restore = await createRestoreFromJsonFn<BlogPost>();
    const mockPost = await createMockTypeFn<BlogPost>();

    // Generate mock data
    const post = mockPost();

    // Validate
    if (isPost(post)) {
        // Serialize to JSON (does not mutate original)
        const json = stringify(post);

        // Deserialize
        const parsed = JSON.parse(json);
        const restored = restore(parsed);
        // restored.publishedAt is a Date
        // restored.metadata is a Map
    } else {
        const errors = getErrors(post);
        console.log('Validation failed:', errors);
    }
}

Advanced: runType and reflectFunction

For advanced use cases, you can access the underlying RunType instance directly.

runType<T>()

Creates a RunType instance from any TypeScript type. Provides access to type metadata and low-level operations.

function runTypeExample() {
    const userRunType = runType<User>();
    // Access type metadata, children, etc.
}

reflectFunction<Fn>()

Reflects a function to get type information about its parameters and return type.

function createUser(name: string, age: number): User {
    return {id: '123', name, createdAt: new Date()};
}

function reflectFunctionExample() {
    const fnReflection = reflectFunction(createUser);
    // Access parameter types, return type, etc.
}

Serialization Details

The any Type

When an any type is encountered during serialization, run-types uses a best-effort approach:

JSON Serialization:

  • Uses JSON.stringify to serialize the value
  • Uses JSON.parse to deserialize the value

Binary Serialization:

  • The value is first converted to a JSON string using JSON.stringify
  • The JSON string is then stored in the binary format
  • During deserialization, JSON.parse is used to restore the value
import {createToBinaryFn, createFromBinaryFn} from '@mionkit/run-types';

type FlexibleData = {
    id: number;
    payload: any; // Can contain any JSON-compatible value
};

async function anyTypeExample() {
    const toBinary = await createToBinaryFn<FlexibleData>();
    const fromBinary = await createFromBinaryFn<FlexibleData>();

    const data: FlexibleData = {
        id: 1,
        payload: {nested: {deeply: [1, 2, 3]}, flag: true},
    };

    const binary = toBinary(data);
    const restored = fromBinary(binary);
    // restored.payload is parsed back from JSON
}
Using any type has performance implications since JSON serialization is less efficient than typed binary encoding. When possible, define specific types for better performance and type safety.

Union Types

Union types allow a value to be one of several possible types. Run-types provides full support for union validation and serialization.

any or unknown types are not allowed in unions, ie: any | User will alway match as any and it is not practical.

How Union Validation Works

When validating a union type, run-types checks each member of the union in declaration order using a first-match strategy. The value is tested against each type until a match is found:

type MyUnion = string | number | boolean;

// Validation order:
// 1. Check if value is string → matches "hello"
// 2. Check if value is number → matches 42
// 3. Check if value is boolean → matches true

For object types in unions, run-types uses loose matching:

  • Objects with properties from multiple union types match the first compatible type
  • Extra properties are allowed (they don't cause validation to fail)
  • For all-optional types (weak types), at least one matching property or an empty object is required
type Cat = {name: string; meow?: true};
type Dog = {name: string; bark?: true};
type Pet = Cat | Dog;

// {name: 'Fluffy', meow: true} matches Cat first
// {name: 'Rex', bark: true} matches Dog first
// {name: 'Unknown', age: 5} matches Cat (extra 'age' is allowed)
Quick Tip
Use the ESLint rules @mionkit/no-unreachable-union-types and @mionkit/no-mixed-union-properties to detect overlapping union types at compile time.

Union Serialization Format

Union types are serialized as a tuple [unionTypeIndex, value] where:

  • unionTypeIndex is the 0-based index of the matching type in the union declaration
  • value is the serialized value according to that type's serialization rules

Example:

type MyUnion = string | number | bigint;

const val1: MyUnion = "hello";  // Encoded as: [0, "hello"]
const val2: MyUnion = 42;       // Encoded as: [1, 42]
const val3: MyUnion = 123n;     // Encoded as: [2, "123n"] (bigint as string)

This encoding is necessary because some types cannot be distinguished after serialization. For example, both string and bigint serialize to strings in JSON, so the discriminator index ensures correct deserialization.

import {createPrepareForJsonFn, createRestoreFromJsonFn} from '@mionkit/run-types';

type Result = string | number | {error: string};

async function unionTypeExample() {
    const prepareForJson = await createPrepareForJsonFn<Result>();
    const restoreFromJson = await createRestoreFromJsonFn<Result>();

    // String value (index 0)
    const json1 = prepareForJson('hello');
    // Returns: [0, 'hello']

    // Number value (index 1)
    const json2 = prepareForJson(42);
    // Returns: [1, 42]

    // Object value (index 2)
    const json3 = prepareForJson({error: 'not found'});
    // Returns: [2, {error: 'not found'}]

    // Deserialization restores the correct type
    const restored = restoreFromJson(json2);
    // restored === 42
}

Union Restrictions

Unions cannot contain the following types:

  • any or unknown - These types match everything at runtime, making union discrimination impossible
  • Non-serializable types like Symbol or Function - These cannot be properly serialized
// ❌ Invalid: Union with 'any' or 'unknown'
type BadUnion = any | string;      // Error: Union can not have 'any' or 'unknown' types
type BadUnion2 = unknown | number; // Error: Union can not have 'any' or 'unknown' types

// ✅ Valid: Union with concrete types
type GoodUnion = string | number | boolean;
Runtime type checking is used to discriminate the union type during serialization. For unions with many types or complex nested structures, this can have performance implications. Use unions carefully in performance-critical code paths.

Type Compiler

RunTypes relies on @deepkit/type-compiler to generate type metadata at compile time. This TypeScript transformer embeds bytecode into the compiled JavaScript, enabling runtime access to type information without requiring separate schema definitions.

Deepkit is a framework that uses Typescript as single source of truth. While Deepkit offers a full enterprise framework with ORM, HTTP, RPC, and more, mion only uses their type compiler for generating type metadata.

Enabling Type Metadata

To enable type metadata generation, add reflection: true to your tsconfig.json:

tsconfig.json
{
  "compilerOptions": {
    // ... your compiler options
  },
  "reflection": true
}

This setting instructs the type compiler to generate bytecode metadata for all types in your project.

Disabling Metadata for Specific Nodes

In some cases, you may want to exclude specific functions, classes, or types from metadata generation. Use the @reflection JSDoc tag to control this:

/** @reflection never */
export function myInternalFunction() {
  // No type metadata will be generated for this function
  return function innerFn(value: string): boolean {
    return value.length > 0;
  };
}

The @reflection tag accepts the following values:

ValueEffect
never, no, false, disabled, 0Disables metadata generation
true, default, enabled, 1, or emptyEnables metadata generation

This is particularly useful for:

  • Pure functions used in JIT compilation that should not include compiler artifacts
  • Internal utilities that don't need runtime type information
  • Performance optimization by reducing generated code size

Example: Pure Function for UUID Validation

Pure functions are methods that do not have side effect, don't depend on variables outside their scope or module imports and can be embedded into JIT-compiled code. They must use @reflection never to avoid including type compiler artifacts.

Pure functions are registered with a namespace to organize and group related functions. This allows different libraries or modules to register their own pure functions without naming conflicts:

import {GenericPureFunction, registerPureFnClosure} from '@mionkit/core';

/** @reflection never */
export function isOdd() {
    return function _isOdd(value: string): boolean {
        return value.length > 0;
    } as GenericPureFunction<any>;
}

// Register the pure function with a namespace for use in JIT compilation
registerPureFnClosure('myNamespace', isOdd);