RunTypes Overview
@mionjs/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
Validation Functions
createIsTypeFn<T>()
Returns a function that checks if a value matches the type. Returns true or false.
import {createIsTypeFn} from '@mionjs/run-types';
interface User {
name: string;
age: number;
}
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 '@mionjs/run-types';
interface User {
name: string;
age: number;
}
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 '@mionjs/run-types';
interface Event {
name: string;
timestamp: Date;
metadata: Map<string, any>;
}
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 '@mionjs/run-types';
interface Event {
name: string;
timestamp: Date;
metadata: Map<string, any>;
}
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 '@mionjs/run-types';
interface Event {
name: string;
timestamp: Date;
metadata: Map<string, any>;
}
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).
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.
const fromBinary = await createFromBinaryFn<User>();
const bufferInput = new Uint8Array(); // from previous example
const user = fromBinary(bufferInput);
// 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;
}
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 {FormatEmail} from '@mionjs/type-formats/StringFormats';
import {FormatPositiveInt} from '@mionjs/type-formats/NumberFormats';
interface ValidatedUser {
email: FormatEmail;
followersCount: FormatPositiveInt;
}
const mockValidatedUser = await createMockTypeFn<ValidatedUser>();
const validatedUser = mockValidatedUser();
// { email: 'user@example.com', followersCount: 150 }
Complete Example
import {
createIsTypeFn,
createTypeErrorsFn,
createStringifyJsonFn,
createRestoreFromJsonFn,
createMockTypeFn,
} from '@mionjs/run-types';
interface BlogPost {
id: string;
title: string;
content: string;
author: {
name: string;
email: string;
};
tags: string[];
publishedAt: Date;
metadata: Map<string, any>;
}
// Create all needed functions
const isPost = await createIsTypeFn<BlogPost>();
const getPostErrors = await createTypeErrorsFn<BlogPost>();
const stringifyPost = await createStringifyJsonFn<BlogPost>();
const restorePost = 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 = stringifyPost(post);
// Deserialize
const parsed = JSON.parse(json);
const restored = restorePost(parsed);
// restored.publishedAt is a Date
// restored.metadata is a Map
} else {
const errors = getPostErrors(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.stringifyto serialize the value - Uses
JSON.parseto 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.parseis used to restore the value
import {createToBinaryFn, createFromBinaryFn} from '@mionjs/run-types';
type FlexibleData = {
id: number;
payload: any; // Can contain any JSON-compatible value
};
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
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)
Use the ESLint rules
@mionjs/no-unreachable-union-types and @mionjs/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:
unionTypeIndexis the 0-based index of the matching type in the union declarationvalueis 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 '@mionjs/run-types';
type Result = string | number | {error: string};
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:
anyorunknown- These types match everything at runtime, making union discrimination impossible- Non-serializable types like
SymbolorFunction- 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;
Type Compiler
RunTypes relies on a TypeScript type compiler to generate type metadata at compile time. This transformer embeds bytecode into the compiled JavaScript, enabling runtime access to type information without requiring separate schema definitions. The mion Vite plugin handles this transformation automatically.
Enabling Type Metadata
To enable type metadata generation, add reflection: true to your 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:
| Value | Effect |
|---|---|
never, no, false, disabled, 0 | Disables metadata generation |
true, default, enabled, 1, or empty | Enables 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 '@mionjs/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);