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 @mionkit/type-formats/FormatsString:

import {StrEmail, StrEmailStrict, StrEmailPunycode} from '@mionkit/type-formats/FormatsString';

Usage Examples

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

type UserEmail = StrEmail;

// 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 StrFormat

Use StrFormat to define custom string constraints:

import {StrFormat} from '@mionkit/type-formats/FormatsString';

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

// Slug with pattern validation
const slugRegex = /^[a-z0-9-]+$/;
type Slug = StrFormat<{
    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 = StrFormat<{
    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 @mionkit/type-formats/FormatsNumber:

import {
    NumFormat,
    NumInteger,
    NumFloat,
    NumPositive,
    NumNegative,
    NumPositiveInt,
    NumNegativeInt,
    NumInt8,
    NumInt16,
    NumInt32,
    NumUInt8,
    NumUInt16,
    NumUInt32,
} from '@mionkit/type-formats/FormatsNumber';

Usage Examples

import {NumFormat} from '@mionkit/type-formats/FormatsNumber';

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

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

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

Custom Number Formats with NumFormat

import {NumFormat} from '@mionkit/type-formats/FormatsNumber';

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

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

// Price must be multiple of 0.01 (cents)
type Price = NumFormat<{
    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 '@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);
    }
}

Using with Validation

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

Binary Serialization Optimization

Use Number formats with specific ranges enable optimized binary serialization:

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

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

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

// start-from-binary
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 }
}
// 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 StrUUIDv4uuid 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 {StrEmail, StrUUIDv4} from '@mionkit/type-formats/FormatsString';

type User = {
    email: StrEmail;      // Branded with 'email'
    id: StrUUIDv4;        // Branded with 'uuid'
};

// ❌ TypeScript Error: Type 'string' is not assignable to type 'StrEmail'
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 '@mionkit/core';

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

Optional Branding with StrFormat, NumFormat, BigNumFormat

StrFormat, NumFormat, and BigNumFormat are unbranded by default (escape hatch), but you can add branding:

import type {StrFormat} from '@mionkit/type-formats/FormatsString';
import type {NumFormat} from '@mionkit/type-formats/FormatsNumber';

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

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

Note: Built-in formats like StrEmail, StrUUIDv4, StrUrl, 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 @mionkit/type-formats (which includes runtime validation code). Instead, import branded types from @mionkit/core:

Server → Client Type Mapping

Server Type (type-formats)Client Type (core)
StrEmailBrandEmail
StrUUIDv4BrandUUID
StrUUIDv7BrandUUID
StrUrl, StrUrlHttp, StrUrlFile, StrUrlSocialMediaBrandUrl
StrDomain, StrDomainStrictBrandDomain
StrIP, StrIPv4, StrIPv6, etc.BrandIP
StrDateBrandDate
StrTimeBrandTime
StrDateTimeBrandDateTime
StrFormat❌ (unbranded by default)
NumFormat❌ (unbranded by default)
BigNumFormat❌ (unbranded by default)

Client Example

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

// These types are compatible with server-side StrEmail, StrUUIDv4, 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,
};