Type Formats

Overview

Add rules like email, int32 or a maximum item count to your types.

Atomic and Collection Type Formats

A type format adds a rule to a type. An atomic format checks one value and replaces its type: TF.Email instead of string. A collection format wraps an array, object, Set or Map you already wrote and checks the whole, like its size or repeated entries.

FamilyKindExamples
StringatomicTF.Email, TF.UUIDv4, TF.StringUrl, TF.Url, TF.CreditCard
NumberatomicTF.Int32, TF.Positive, TF.Currency
BigIntatomicTF.BigInt64, TF.BigPositive
Date and timeatomicTF.StringDate, TF.Date, TFT.PlainDate
CollectionscollectionTF.FormattedArray<string[], {maxItems: 5}>, TF.FormattedMap
An atomic format type is capitalized (TF.Email), and its builder is a call (TF.email()). A collection format has no named presets: its rules are options on the collection.

Declaring a Type Format

Use a format type from the TF namespace, or the matching TF.* builder. Both compile to the same validator, and you can mix them in one file.

import type * as TF from '@mionjs/run-types/formats';
import {createValidateFn} from '@mionjs/run-types';

type Account = {
  id: TF.UUIDv4;
  email: TF.Email;
  age: TF.Int32;
  credits: TF.Positive;
};

const isAccount = createValidateFn<Account>();

isAccount({
  id: '109156be-c4fb-41ea-b1b4-efe1671c5836',
  email: '[email protected]',
  age: 36,
  credits: 100,
}); // true

isAccount({id: 'not-a-uuid', email: 'nope', age: 1.5, credits: -5}); // false

export {isAccount};
export type {Account};
import * as TF from '@mionjs/run-types/formats';
import {createValidateFn, type InferType} from '@mionjs/run-types';
import * as RT from '@mionjs/run-types/builders';

const account = RT.object({
  id: TF.uuidv4(),
  email: TF.email(),
  age: TF.int32(),
  credits: TF.positive(),
});

// InferType<typeof runType> hands the TypeScript type back.
type Account = InferType<typeof account>;

const isAccount = createValidateFn(account);

export {account, isAccount};
export type {Account};

Custom Type Formats

TF.String, TF.Number and TF.BigInt take your own params when no named format fits. multipleOfTolerance widens the rounding a decimal multipleOf allows. With integer: true, multipleOf must be a whole number.

import type * as TF from '@mionjs/run-types/formats';
import {createValidateFn} from '@mionjs/run-types';

type Username = TF.String<{
  minLength: 3;
  maxLength: 20;
  pattern: {source: '^[a-z0-9_]+$'; mockSamples: ['ada_99', 'grace']};
}>;
type Percentage = TF.Number<{min: 0; max: 100}>;
type BigPositive = TF.BigInt<{min: 0n}>;
type Price = TF.Number<{min: 0; multipleOf: 0.01}>;

type Profile = {
  handle: Username;
  completion: Percentage;
  followers: BigPositive;
};

const isProfile = createValidateFn<Profile>();

isProfile({handle: 'ada_99', completion: 80, followers: 1200n}); // true
isProfile({handle: 'no', completion: 150, followers: -1n}); // false

const isPrice = createValidateFn<Price>();

isPrice(19.99); // true, a decimal multipleOf allows tiny float rounding errors
isPrice(19.995); // false

export {isProfile, isPrice};
export type {Profile, Price};
FormatCommon params
TF.StringminLength, maxLength, length, pattern, allowedChars, allowedValues, …
TF.Numbermin, max, gt, lt, integer, float, multipleOf, multipleOfTolerance, isCurrency
TF.BigIntmin, max, gt, lt, multipleOf (all bigint literals, e.g. 0n)
min / max are inclusive, gt / lt exclusive. Use one or the other per edge: the build rejects a type with both, such as {min: 0, gt: 0}. Date formats take the same four bounds.

Branded Types

By default an atomic format is still structurally a string or number, so a raw string fits where a UserId belongs. Add a brand name as the second type argument and nothing else fits without an as cast. With builders, use the brand() tag: TF.string({minLength: 1}, TF.brand('UserId')).

import type * as TF from '@mionjs/run-types/formats';

type UserId = TF.String<{minLength: 1}, 'UserId'>;
type Cents = TF.Number<{min: 0; integer: true}, 'Cents'>;

// a bare string would not fit
const id = 'usr_abc123' as UserId;
const price = 4999 as Cents;

// Now UserId and Cents don't mix with each other or with raw string/number.
function chargeUser(_user: UserId, _amount: Cents): void {}
chargeUser(id, price); // ok

export {id, price, chargeUser};
export type {UserId, Cents};
Put the as cast where a raw value becomes a UserId, usually right after you validate it. Everywhere else, TypeScript keeps your ids, cents and timestamps apart.
Copyright © 2026