RunTypes

Type Formats

Validation and transformation formats for strings and numbers using TypeScript types.

Type Formats extend TypeScript's type system with validation, transformation, and serialization rules. Instead of writing validation schemas separately, you express constraints directly in your types.

Overview

Type Formats provide:

  • Validation: Min/max length, patterns, allowed values
  • Serialization: Optimized binary encoding for numeric types
  • Mock Generation: Type-aware mock data with valid values
  • ⚠️ Transformation: Trim, lowercase, uppercase, capitalize (not implemented yet)

String Formats

Built-in String Formats

Import string formats from @mionjs/type-formats/StringFormats:

import {FormatEmail, FormatEmailStrict, FormatEmailPunycode} from '@mionjs/type-formats/StringFormats';

Usage Examples

import {FormatEmail} from '@mionjs/type-formats/StringFormats';

type UserEmail = FormatEmail;

// Valid
('user@example.com'); // ✓
('user+tag@example.com'); // ✓ (allows + for email aliases)
('user(comment)@test.com'); // ✓

// Invalid
('user@name@example.com'); // ✗ (multiple @)
('@example.com'); // ✗ (missing local part)

Custom String Formats with FormatString

Use FormatString to define custom string constraints:

import {FormatString} from '@mionjs/type-formats/StringFormats';

// Username: 3-20 chars, lowercase, trimmed
type Username = FormatString<{
    minLength: 3;
    maxLength: 20;
    lowercase: true;
    trim: true;
}>;

// Slug with pattern validation
const slugRegex = /^[a-z0-9-]+$/;
type Slug = FormatString<{
    minLength: 1;
    maxLength: 100;
    pattern: {
        val: typeof slugRegex;
        errorMessage: 'Slug can only contain lowercase letters, numbers, and hyphens';
        mockSamples: ['my-post', 'hello-world', 'article-123'];
    };
}>;

// Name with allowed characters only
type SafeName = FormatString<{
    minLength: 1;
    maxLength: 50;
    allowedChars: {
        val: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ';
        errorMessage: 'Name can only contain letters and spaces';
    };
    capitalize: true;
}>;

String Format Parameters

ParameterTypeDescription
minLengthnumberMinimum string length
maxLengthnumberMaximum string length
pattern{val: RegExp; errorMessage: string; mockSamples: string[]}Regex validation
allowedChars{val: string; errorMessage: string}Allowed characters
disallowedChars{val: string; errorMessage: string}Disallowed characters
allowedValues{val: string[]; errorMessage: string}Enum-like validation
lowercasebooleanTransform to lowercase
uppercasebooleanTransform to uppercase
capitalizebooleanCapitalize first letter
trimbooleanTrim whitespace

Number Formats

Built-in Number Formats

Import number formats from @mionjs/type-formats/NumberFormats:

import {
    FormatNumber,
    FormatInteger,
    FormatFloat,
    FormatPositive,
    FormatNegative,
    FormatPositiveInt,
    FormatNegativeInt,
    FormatInt8,
    FormatInt16,
    FormatInt32,
    FormatUInt8,
    FormatUInt16,
    FormatUInt32,
} from '@mionjs/type-formats/NumberFormats';

Usage Examples

import {FormatNumber} from '@mionjs/type-formats/NumberFormats';

// Age with valid range
type Age = FormatNumber<{
    min: 0;
    max: 120;
    integer: true;
}>;

// Percentage with decimals
type Percentage = FormatNumber<{
    min: 0;
    max: 100;
}>;

// Price must be multiple of 0.01 (cents)
type Price = FormatNumber<{
    min: 0;
    multipleOf: 1; // multipleOf must be integer
    integer: true; // store as cents
}>;

Custom Number Formats with FormatNumber

import {FormatNumber} from '@mionjs/type-formats/NumberFormats';

// Age with valid range
type Age = FormatNumber<{
    min: 0;
    max: 120;
    integer: true;
}>;

// Percentage with decimals
type Percentage = FormatNumber<{
    min: 0;
    max: 100;
}>;

// Price must be multiple of 0.01 (cents)
type Price = FormatNumber<{
    min: 0;
    multipleOf: 1; // multipleOf must be integer
    integer: true; // store as cents
}>;

Number Format Parameters

ParameterTypeDescription
minnumberMinimum value
maxnumberMaximum value
integerbooleanMust be an integer
floatbooleanExplicitly a float (affects binary serialization)
multipleOfnumberMust be a multiple of this value

Complete Example

Here's a comprehensive example demonstrating various type formats:

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);
}

Using with Validation

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' }
// ]

Binary Serialization Optimization

Use Number formats with specific ranges enable optimized binary serialization:

import {createToBinaryFn, createFromBinaryFn} from '@mionjs/run-types';

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

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

// start-from-binary
const fromBinary = await createFromBinaryFn<User>();
const bufferInput = new Uint8Array(); // from previous example
const user = fromBinary(bufferInput);
// user is now { name: 'John', age: 30 }
// end-from-binary

Branded Types (Nominal Typing)

Type Formats support branded types for nominal typing. This prevents accidental assignment of plain strings/numbers to validated types, providing compile-time type safety.

Why Branded Types?

Most built-in formats use branded types with fixed brand names for several important reasons:

  1. Type Safety: Prevents accidental assignment of plain strings to validated types at compile time
  2. Database Mapping: Enables automatic translation of branded types to appropriate database columns (e.g., the Drizzle plugin can map FormatUUIDv4uuid column type)
  3. Friendly Errors: Allows better type mapping for user-friendly error messages (e.g., "Invalid email format" instead of generic "Invalid string")
  4. Consistent API: Ensures predictable behavior across the framework's ecosystem

How Branding Works

Most built-in string formats are branded by default:

import type {FormatEmail, FormatUUIDv4} from '@mionjs/type-formats/StringFormats';

type User = {
    email: FormatEmail;      // Branded with 'email'
    id: FormatUUIDv4;        // Branded with 'uuid'
};

// ❌ TypeScript Error: Type 'string' is not assignable to type 'FormatEmail'
const user: User = {
    email: 'test@example.com',  // Error!
    id: '550e8400-e29b-41d4-a716-446655440000'  // Error!
};

Using Branded Types

To assign values to branded types, use type assertions:

import type {BrandEmail, BrandUUID} from '@mionjs/core';

const user: User = {
    email: 'test@example.com' as BrandEmail,
    id: '550e8400-e29b-41d4-a716-446655440000' as BrandUUID,
};

Optional Branding with FormatString, FormatNumber, FormatBigInt

FormatString, FormatNumber, and FormatBigInt are unbranded by default (escape hatch), but you can add branding:

import type {FormatString} from '@mionjs/type-formats/StringFormats';
import type {FormatNumber} from '@mionjs/type-formats/NumberFormats';

// Unbranded - accepts plain strings/numbers
type DisplayName = FormatString<{minLength: 2; maxLength: 50}>;
type Age = FormatNumber<{min: 0; max: 150; integer: true}>;

// Branded - requires type assertion
type UserName = FormatString<{minLength: 2; maxLength: 50}, 'UserName'>;
type UserId = FormatNumber<{min: 1; integer: true}, 'UserId'>;

Note: Built-in formats like FormatEmail, FormatUUIDv4, FormatUrl, etc. have fixed brand names that cannot be overridden. This ensures consistent type mapping across the framework.


Client-Side Usage (Without type-formats)

Client code should not depend on @mionjs/type-formats (which includes runtime validation code). Instead, import branded types from @mionjs/core:

Server → Client Type Mapping

Server Type (type-formats)Client Type (core)
FormatEmailBrandEmail
FormatUUIDv4BrandUUID
FormatUUIDv7BrandUUID
FormatUrl, FormatUrlHttp, FormatUrlFile, FormatUrlSocialMediaBrandUrl
FormatDomain, FormatDomainStrictBrandDomain
FormatIP, FormatIPv4, FormatIPv6, etc.BrandIP
FormatStringDateBrandDate
FormatStringTimeBrandTime
FormatStringDateTimeBrandDateTime
FormatString❌ (unbranded by default)
FormatNumber❌ (unbranded by default)
FormatBigInt❌ (unbranded by default)

Client Example

// client-types.ts
import type {BrandEmail, BrandUUID, BrandUrl} from '@mionjs/core';

// These types are compatible with server-side FormatEmail, FormatUUIDv4, etc.
type User = {
    id: BrandUUID;
    email: BrandEmail;
    website?: BrandUrl;
};

// Usage
const user: User = {
    id: '550e8400-e29b-41d4-a716-446655440000' as BrandUUID,
    email: 'user@example.com' as BrandEmail,
    website: 'https://example.com' as BrandUrl,
};