Type Formats

Collection Formats

Size, uniqueness and key rules for arrays, objects, Sets and Maps.

Declaring a Collection Format

Arrays, objects, Maps and Sets keep the shape you wrote in TypeScript. Limits on the collection itself go in one options object, with one wrapper per collection kind.

type Tags = TF.FormattedArray<
  string[],
  {minItems: 1; maxItems: 5; uniqueItems: true}
>;

const isTags = createValidateFn<Tags>();
const isTagsBuilt = createValidateFn(
  RT.array(TF.string(), {minItems: 1, maxItems: 5, uniqueItems: true})
);

isTags(['ada', 'grace']); // true
isTags([]); // false, at least one entry
isTagsBuilt(['ada', 'ada']); // false, the entries repeat

// The two spellings are one generated function, not two that agree.
getRunTypeId<Tags>() ===
  getRunTypeId(
    RT.array(TF.string(), {minItems: 1, maxItems: 5, uniqueItems: true})
  ); // true
Type-firstType Builder
Arrays and tuplesTF.FormattedArray<T[], {...}>RT.array(item, {...})
Objects and recordsTF.FormattedObject<Shape, {...}>RT.object(config, {...}), RT.record(inner, {...})
SetsTF.FormattedSet<Set<T>, {...}>RT.set(item, {...})
MapsTF.FormattedMap<Map<K, V>, {...}>RT.map(key, value, {...})
No TypeScript type says "an array whose entries differ", so uniqueItems keeps the closest shape (string[]) and the generated function does the check.

Collection Options

An entry is one item of an array, one member of a Set, or one [key, value] pair of a Map. On a Map, put unknown in the half of contains you do not care about.

// `contains` takes a type in a type, and a run-type in a builder
type WithAdminId = TF.FormattedArray<
  string[],
  {contains: TF.UUID; minContains: 2}
>;

const hasTwoIds = createValidateFn<WithAdminId>();
const hasTwoIdsBuilt = createValidateFn(
  RT.array(TF.string(), {contains: TF.uuid(), minContains: 2})
);

hasTwoIds([
  'plain',
  '9f1b8c2e-3d4a-4b5c-8d6e-1f2a3b4c5d6e',
  '018f1b8c-2e3d-7b5c-8d6e-1f2a3b4c5d6e',
]); // true
hasTwoIdsBuilt(['plain', '9f1b8c2e-3d4a-4b5c-8d6e-1f2a3b4c5d6e']); // false, only one matches
OptionOnWhat it asks
minItems / maxItemsevery collectionhow many entries there may be
uniqueItemsevery collectionno two entries are equal, compared by value
containsevery collectionat least one entry matches a second type
minContains / maxContainsevery collectionhow many entries must match contains
minProperties / maxPropertiesobjectshow many keys the value may carry
patternPropertiesobjectskeys matching a pattern carry a given type
propertyNamesobjectsevery key satisfies a string format
These are JSON Schema's own options and names, so a generated JSON Schema document enforces exactly what the type does. A bounded Set or Map declares them on the array it travels as.

Object Size Options

minProperties and maxProperties limit how many keys an object carries.

// Objects work the same way: the shape is the type, and everything else about
// the keys is an option.
interface Settings {
  theme: string;
  locale?: string;
}

type BoundedSettings = TF.FormattedObject<
  Settings,
  {minProperties: 1; maxProperties: 2}
>;

const isSettings = createValidateFn<BoundedSettings>();
const isSettingsBuilt = createValidateFn(
  RT.object(
    {theme: TF.string(), locale: RT.optional(TF.string())},
    {minProperties: 1, maxProperties: 2}
  )
);

isSettings({theme: 'dark'}); // true
isSettingsBuilt({theme: 'dark', locale: 'en'}); // true

Object Key Options

patternProperties gives a type to every key that matches a pattern, and propertyNames checks every key against a string format.

type Columns = TF.FormattedObject<
  Record<string, unknown>,
  {patternProperties: {'^col_': number}}
>;
type LowercaseKeys = TF.FormattedObject<
  Record<string, string>,
  {propertyNames: TF.Alpha}
>;

const isColumns = createValidateFn<Columns>();
const isLowercaseKeys = createValidateFn<LowercaseKeys>();

isColumns({col_width: 40}); // true
isColumns({col_width: 'wide'}); // false, a col_ key must hold a number
isLowercaseKeys({theme: 'dark'}); // true
isLowercaseKeys({'theme-2': 'dark'}); // false, the key is not alphabetic

Set and Map Options

On a Set or Map, uniqueItems matters when the members are objects: it rejects two Set members, or two Map keys, with the same content.

// same options as an array; on a Map, `contains` and `uniqueItems` use [key, value] pairs
type Labels = TF.FormattedSet<Set<string>, {minItems: 1; maxItems: 5}>;
type Points = TF.FormattedSet<Set<{x: number; y: number}>, {uniqueItems: true}>;
type Scores = TF.FormattedMap<Map<string, number>, {maxItems: 100}>;
type WithAdmin = TF.FormattedMap<
  Map<string, number>,
  {contains: ['admin', unknown]}
>;
type Seats = TF.FormattedMap<Map<{row: number}, string>, {uniqueItems: true}>;

const isLabels = createValidateFn<Labels>();
const isPoints = createValidateFn<Points>();
const isScores = createValidateFn<Scores>();
const isWithAdmin = createValidateFn<WithAdmin>();
const isSeats = createValidateFn<Seats>();
const isScoresBuilt = createValidateFn(
  RT.map(TF.string(), TF.number(), {maxItems: 100})
);

isLabels(new Set(['ada'])); // true
isLabels(new Set()); // false, at least one label
isPoints(
  new Set([
    {x: 1, y: 2},
    {x: 1, y: 2},
  ])
); // false, two equal points
isWithAdmin(new Map([['admin', 1]])); // true
isWithAdmin(new Map([['guest', 1]])); // false, no admin entry
isSeats(
  new Map([
    [{row: 1}, 'ada'],
    [{row: 1}, 'ada'],
  ])
); // false, the same pair twice
isScoresBuilt(new Map([['ada', 10]])); // true

getRunTypeId<Scores>() ===
  getRunTypeId(RT.map(TF.string(), TF.number(), {maxItems: 100})); // true
Copyright © 2026