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;
}- Array of strings:
validate: ["email", "maxLength(255)"] - Array of objects: adds a custom
messageper validator - 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); // trueOn 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
| Validator | Checks |
|---|---|
email | Valid email address |
url | Parses as a URL (URL.canParse), any scheme |
uuid | Valid 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 |
nonEmpty | Not the empty string |
trimmed | No leading/trailing whitespace |
lowercase | All lowercase |
uppercase | All uppercase |
capitalized | First character uppercase |
uncapitalized | First character lowercase |
startsWith(s) | Has the prefix |
endsWith(s) | Has the suffix |
includes(s) | Contains the substring |
Number validators
| Validator | Checks |
|---|---|
greaterThan(n) | > n |
greaterThanOrEqualTo(n) | ≥ n |
lessThan(n) | < n |
lessThanOrEqualTo(n) | ≤ n |
between(min, max) | Within range |
int | Integer |
nonNegativeInt | Integer and ≥ 0 |
uint8 | Integer 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 |
finite | Not Infinity, -Infinity or NaN |
nonNaN | Not NaN |
BigInt validators
| Validator | Checks |
|---|---|
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:
| Validator | Checks |
|---|---|
validDate | Parses 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
| Validator | Checks |
|---|---|
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: "" });