Type Formats
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
| Parameter | Type | Description |
|---|---|---|
minLength | number | Minimum string length |
maxLength | number | Maximum 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 |
lowercase | boolean | Transform to lowercase |
uppercase | boolean | Transform to uppercase |
capitalize | boolean | Capitalize first letter |
trim | boolean | Trim 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
| Parameter | Type | Description |
|---|---|---|
min | number | Minimum value |
max | number | Maximum value |
integer | boolean | Must be an integer |
float | boolean | Explicitly a float (affects binary serialization) |
multipleOf | number | Must 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:
- Type Safety: Prevents accidental assignment of plain strings to validated types at compile time
- Database Mapping: Enables automatic translation of branded types to appropriate database columns (e.g., the Drizzle plugin can map
FormatUUIDv4→uuidcolumn type) - Friendly Errors: Allows better type mapping for user-friendly error messages (e.g., "Invalid email format" instead of generic "Invalid string")
- 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) |
|---|---|
FormatEmail | BrandEmail |
FormatUUIDv4 | BrandUUID |
FormatUUIDv7 | BrandUUID |
FormatUrl, FormatUrlHttp, FormatUrlFile, FormatUrlSocialMedia | BrandUrl |
FormatDomain, FormatDomainStrict | BrandDomain |
FormatIP, FormatIPv4, FormatIPv6, etc. | BrandIP |
FormatStringDate | BrandDate |
FormatStringTime | BrandTime |
FormatStringDateTime | BrandDateTime |
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,
};