API Stability Notice

Macroforge is under active development. The API is not yet stable and may change between versions. Some documentation sections may be outdated.

Validators

Validators run during decoding. Failures are reported through the errors array of the result union rather than thrown, so a single call reports every problem at once.

Three ways to apply them

/** @derive(Decode) */
class User {
  /** @endec(validate: ["email", "maxLength(255)"]) */
  email: string;

  /** @endec(validate: [{ validate: "minLength(2)", message: "Name too short" }]) */
  name: string;

  /** @endec(minLength(8), maxLength(64)) */
  password: string;
}
  1. Array of strings: validate: ["email", "maxLength(255)"]
  2. Array of objects: adds a custom message per validator
  3. Shorthand: bare validator calls directly in @endec(...)

Validators check the declared type’s value. On a nullable or optional field (string | null, name?: string) a null or absent value is not checked, and a present value is checked like any other. On any other field a null value fails with “is required”.

validate must be an array. An object literal such as validate: { email: true } is rejected with “validate must be an array”.

Misspelled names are caught with a suggestion: the parser does a Levenshtein match against the known list and tells you what you probably meant.

On a primitive alias

Validators can also go on a type alias of a primitive, plain or branded with $Newtype. They then validate the value itself, and every field typed with the alias gets them through the alias’s decode:

/** @derive(Encode, Decode) */
/** @endec(nonNegative, finite) */
type Meters = $Newtype<number>;

Meters.decode(-1); // { success: false, errors: [{ field: "_root", message: "Meters must be non-negative" }] }
Meters.is(12);     // true

On a field such as distance: Meters, the error’s field is distance. Alias-level validators on any other alias shape are rejected at expansion; put them on the fields.

On a union arm

A primitive arm of a union alias can carry its own validators in a leading comment. They run only when the value takes that arm:

/** @derive(Decode) */
type Contact = /** @endec(email) */ string | number;

Contact.decode("not-an-email"); // { success: false, errors: [{ field: "_root", message: "Contact must be a valid email" }] }
Contact.decode(42);             // { success: true, value: 42 }

Validators on any other arm are rejected at expansion; put them on the variant’s fields.

String validators

ValidatorChecks
emailValid email address
urlParses as a URL (URL.canParse), any scheme
uuidValid UUID
pattern("re")Matches the regular expression
minLength(n)Length ≥ n
maxLength(n)Length ≤ n
length(n)Length exactly n
length(min, max)Length within range
nonEmptyNot the empty string
trimmedNo leading/trailing whitespace
lowercaseAll lowercase
uppercaseAll uppercase
capitalizedFirst character uppercase
uncapitalizedFirst character lowercase
startsWith(s)Has the prefix
endsWith(s)Has the suffix
includes(s)Contains the substring

Number validators

ValidatorChecks
greaterThan(n)> n
greaterThanOrEqualTo(n)≥ n
lessThan(n)< n
lessThanOrEqualTo(n)≤ n
between(min, max)Within range
intInteger
nonNegativeIntInteger and ≥ 0
uint8Integer in 0–255
multipleOf(n)Divisible by n, exactly for decimals (0.3 is a multiple of 0.1); n must be finite and non-zero
positive> 0
nonNegative≥ 0
negative< 0
nonPositive≤ 0
finiteNot Infinity, -Infinity or NaN
nonNaNNot NaN

BigInt validators

ValidatorChecks
positiveBigInt> 0n
nonNegativeBigInt≥ 0n
negativeBigInt< 0n
nonPositiveBigInt≤ 0n
greaterThanBigInt(n)> n
greaterThanOrEqualToBigInt(n)≥ n
lessThanBigInt(n)< n
lessThanOrEqualToBigInt(n)≤ n
betweenBigInt(min, max)Within range

Date validators

Note the names: they are comparison-shaped, not after/before:

ValidatorChecks
validDateParses to a valid Date
greaterThanDate(d)After d
greaterThanOrEqualToDate(d)At or after d
lessThanDate(d)Before d
lessThanOrEqualToDate(d)At or before d
betweenDate(start, end)Within range

Array validators

ValidatorChecks
minItems(n)At least n elements
maxItems(n)At most n elements
itemsCount(n)Exactly n elements

Custom validators

custom(fnName) calls a function you supply, which should return true when the value is valid. The function must be in scope in the file: declared there, or imported by it.

/** @endec(validate: ["custom(isStrongPassword)"]) */
password: string;

To use a function from another module without importing it yourself, pass an object with the function to call and the source module it comes from. The expansion imports it under a private name, so the import never looks unused to a linter and cannot be removed by one. source is written as you would write it in an import in that file.

/** @endec(validate: ['custom({ function: "isStrongPassword", source: "./validators" })']) */
password: string;

Validating without decoding

Generated types also expose the validators directly, which is useful for form validation. They take input that has not been decoded yet, such as a form’s draft, so a value need not satisfy the field’s type to be checked:

User.validateField("email", "not-an-email");
User.validateFields({ email: "a@b.com", name: "" });