Overview
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.
| Family | Kind | Examples |
|---|---|---|
| String | atomic | TF.Email, TF.UUIDv4, TF.StringUrl, TF.Url, TF.CreditCard |
| Number | atomic | TF.Int32, TF.Positive, TF.Currency |
| BigInt | atomic | TF.BigInt64, TF.BigPositive |
| Date and time | atomic | TF.StringDate, TF.Date, TFT.PlainDate |
| Collections | collection | TF.FormattedArray<string[], {maxItems: 5}>, TF.FormattedMap |
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};
| Format | Common params |
|---|---|
TF.String | minLength, maxLength, length, pattern, allowedChars, allowedValues, … |
TF.Number | min, max, gt, lt, integer, float, multipleOf, multipleOfTolerance, isCurrency |
TF.BigInt | min, 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};
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.