Syntax Reference

A detailed description of all available syntax

The following reference contains an overview of all the syntax available. When a schema is parsed, an AST tree is produced and exposed via the ruleset field on a validator instance. The shape of every node type is documented under the "rule structure" sections. Most casual users can ignore this information, but it does come in handy for making type transformers.

Stability Warning: In regards to the "Rule Structure" sections found on this page, please keep in mind that there are no guarantees over the stability of the shape of these interfaces. While breaking changes to these interfaces are somewhat unlikely, it is entirely possible that TypeScript will add new syntax features that, if Moat Maker were to properly support, would require changing the shape of particular AST nodes in non-backward-compatible ways. The SemVer number will still be updated appropriately if breaking changes do come up, but if you can help it, it's advisable to avoid relying on this ruleset feature altogether.

"Simple" Rules

A simple rule allows you to match an input value against a primitive type. The available primitive types are: string, number, bigint, boolean, symbol, null, and undefined. You can also match against object to ensure your value isn't a primitive value at all.

validator`number`.assertMatches(2); // ✓
validator`bigint`.assertMatches(2n); // ✓
validator`null`.assertMatches(null); // ✓

validator`object`.assertMatches({ x: 2 }); // ✓
validator`object`.assertMatches([2, 3]); // ✓
validator`object`.assertMatches(() => {}); // ✓

validator`object`.assertMatches(null); // ✕

Boxed primitives should be avoided, but if a validator ever does receive one, it'll correctly treat it like an object, which means new Boolean(true) does not match validator`true`.

Rule Structure

type SimpleTypeVariant = 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'object' | 'null' | 'undefined';

interface SimpleRule {
  readonly category: 'simple'
  readonly type: SimpleTypeVariant
}

Primitive Literal Rules

A primitive literal rule lets you match against an exact primitive value. Literal syntax is supported for string, numbers, bigints, and booleans (undefined and null are categorized as "simple rules"). All kinds of numeric literals are supported to align with the syntax that TypeScript and JavaScript provide, including scientific notation (e.g. 2.3e7), hexadecimal/octal/binary literals, the use of underscores as a separator (e.g. 123_456), and infinity. Similar to TypeScript, NaN and Infinity are not supported literals, but you can still match against NaN by interpolating it in (e.g. validator`${NaN}`).

validator`'Hello World!'`.assertMatches('Hello World!'); // ✓
validator`-2`.assertMatches(-2); // ✓
validator`0xFF`.assertMatches(255); // ✓
validator`2n`.assertMatches(2n); // ✓
validator`true`.assertMatches(true); // ✓

Warning: As always, avoid comparing decimal numbers or numbers with large magnitudes (numbers outside of the Number.MIN_SAFE_INTEGER to Number.MAX_SAFE_INTEGER range). Computers tend to make minor calculation errors that can prevent such numbers from being equal. Instead of doing an equality check, consider checking if the value is inside a range. You can use the expectCloseTo() recipe to help out.

Rule Structure

interface PrimitiveLiteralRule {
  readonly category: 'primitiveLiteral'
  readonly value: string | number | bigint | boolean
}

Just like with the syntax, you are now allowed to set the value property to NaN, Infinity, or -Infinity.

Any/Unknown "No-Op'' rules

Both any and unknown perform the same action in Moat Maker, which is "do not validate me". It is recommended to just stick to using the unknown type to align with the preference of using unknown over any in TypeScript, but don't treat this suggestion as law. If your validator schema is pretty much a copy-paste of a TypeScript interface, then, by all means, use the any type if your TypeScript interface definition uses it, to help keep the relationship between the two clearer.

validator`unknown`.assertMatches(2); // ✓
validator`unknown`.assertMatches({ x: 2 }); // ✓
validator`unknown`.assertMatches(new Date()); // ✓

validator`any`.assertMatches(2); // ✓
validator`any`.assertMatches({ x: 2 }); // ✓
validator`any`.assertMatches(new Date()); // ✓

Rule Structure

interface NoopRule {
  readonly category: 'noop'
}

Property Rules

Property matching syntax can be used to describe which properties you expect the value being validated to have. The values need not be an object — primitives can match as well when their properties line up with your schema. The only values that can never match a property rule are undefined and null.

Just like in TypeScript, the value being validated may have extra properties that aren't defined by your schema. A property can be marked as optional by adding a question mark before the colon. If any of the properties are getters, their functions will be triggered as needed to verify that they return a value of the correct type.

// ✓
validator`{
  myNumb: number
  myOptionalString?: string
}`.assertMatches({ myNumb: 4, extraProp: true });

Individual properties can be separated from each other via commas, semicolons, or new lines. It's recommended to choose the same separator that you use when defining object types in TypeScript.

// ✓
validator`{
  a: 1
  b: 2,
  c: 3;
}`.assertMatches({ a: 1, b: 2, c: 3 });

To add special characters to a property key, you can quote the key. To use a dynamic value (like a symbol) as a key, you can bracket the key and interpolate your value into the brackets.

const mySymbol = Symbol();

// ✓
validator`{
  'special key': number
  [${mySymbol}]: number
}`.assertMatches({ 'special key': 1, [mySymbol]: 2 });

Index signatures can be used to enforce a rule on all non-inherited properties, regardless of whether or not they're enumerable. The type of an index signature must either be string, number, or symbol.

validator`{ [dimension: string]: number }`
  .assertMatches({ x: 2, y: 3 }); // ✓

validator`{ [index: symbol]: number }`
  .assertMatches({ x: 'xyz', [Symbol()]: 'xyz' }); // ✕

Note that the order in which properties are evaluated should be considered as undefined behavior. The exact details of the evaluation order could change at any point in time.

Rule Structure

// The `Rule` interface represents any rule
// documented on this page.

// Helper interface used by PropertyRule
interface PropertyRuleContentValue {
  readonly optional: boolean
  readonly rule: Rule
}

// Helper interface used by PropertyRule
interface PropertyRuleIndexValue {
  readonly key: Rule
  readonly value: Rule
  // Metadata that doesn't actually affect the rule's behavior.
  // In the `{ [index: string]: number }` example, "index"
  // is the label.
  readonly label: string
}

// The `FrozenMap` class is not publicly exported but instances
// of it can be seen publicly. `validator.fromRuleset()` will
// accept rulesets that contain frozen maps and non-frozen maps.
// Non-frozen maps will be auto-converted to frozen maps, which you can
// see by inspecting the `ruleset` property of the resulting validator.
interface PropertyRule {
  readonly category: 'property'

  // Contains normal key-value pairs,
  // like the "x" field from `{ x: number }`.
  readonly content: (
    FrozenMap<string, PropertyRuleContentValue> |
    Map<string, PropertyRuleContentValue>
  )

  // Contains key-value pairs of dynamic keys,
  // like with `{ [${dynamicKey}]: number }`
  // The numeric keys correspond to an index in the ruleset's
  // interpolated array.
  readonly dynamicContent: (
    FrozenMap<number, PropertyRuleContentValue> |
    Map<number, PropertyRuleContentValue>
  )

  // If the rule definition contains an index signature
  // like `{ [index: string]: number }`,
  // it will be stored here.
  readonly index: PropertyRuleIndexValue | null
}

A FrozenMap is a data structure implemented internally that's intended to mimic the behavior of a normal map except that it does not provide methods to mutate its contents. If new methods come out for the normal Map class, you can expect it to take a bit for FrozenMap to be updated to contain those methods as well.

The hope is for JavaScript to come out with a native frozen-map implementation, at which point this project will switch to using those, instead of an internal implementation of them. There is currently a readonly collections proposal in the works to do just this.

Array Rules

An array rule will verify two things.

  1. The provided value is an array (or it inherits from the Array class).

  2. The contents of the array matches the supplied pattern.

validator`number[]`
  .assertMatches([2, 3.5, Infinity]); // ✓

validator`number[]`
  .assertMatches([2, 'this is not a number']); // ✕

validator`number[]`
  .assertMatches({ 0: 2, 1: 3.5, length: 2 }); // ✕

If the array is a sparse array, the holes are treated as undefined. If you wish to actively prevent sparse arrays instead, you can use the expectNonSparse() recipe.

Rule Structure

// The `Rule` interface represents any rule
// documented on this page.

export interface ArrayRule {
  readonly category: 'array'
  readonly content: Rule
}

Tuple Rules

Tuple syntax is an alternative to array syntax, and is used when the types of the elements in the array depend upon their position, for example, the [number, string] rule states that the first element must be a number, and the second must be a string. It also states that the provided array must contain exactly two items, no more, no less.

Similar to TypeScript, entries at the end of the tuple can be marked as optional by adding a ? (e.g. [number, boolean?, string?] Allows you to provide an array with 1, 2, or 3 items), and an unknown number of additional items can be received via the rest syntax ... (e.g. [boolean, ...number[]] allows you to provide an array with at least one item). Optional entries must appear after required entries, and if rest syntax is used, it must be used at the end.

validator`[number, string]`
  .assertMatches([2, 'a string']); // ✓

validator`[number, boolean?, string?]`
  .assertMatches([2, true]); // ✓
  
validator`[boolean, ...number[]]`
  .assertMatches([true, 1, 2, 3, 4]); // ✓

Just like in TypeScript, you can choose to provide names/labels for tuple entries as follows:

validator`[someNumb: number, someStr: string]`
  .assertMatches([2, 'a string']); // ✓

validator`[someNumb: number, optionalBool?: boolean, alsoOptional?: string]`
  .assertMatches([2, true]); // ✓
  
validator`[someFlag: boolean, ...otherNumbs: number[]]`
  .assertMatches([true, 1, 2, 3, 4]); // ✓

If the array is a sparse array, the holes are treated as undefined. If you wish to actively prevent sparse arrays instead, you can use the expectNonSparse() recipe.

Rule Structure

// The `Rule` interface represents any rule
// documented on this page.

interface TupleRule {
  readonly category: 'tuple'

  // A list of all required entries in the tuple.
  readonly content: readonly Rule[]

  // A list of all optional entries in the tuple.
  readonly optionalContent: readonly Rule[]

  // If rest syntax is used, the rule it applied to will be found here.
  readonly rest: Rule | null

  // Metadata that doesn't actually affect the rule's behavior.
  // If names/labels are provided for each tuple entry,
  // they will be stored here.
  readonly entryLabels: readonly string[] | null
}

Union Rules

Union syntax can be used when you need to provide multiple possible types a value can conform to.

validator`number | string`
  .assertMatches(2); // ✓

Rule Structure

// The `Rule` interface represents any rule
// documented on this page.

interface UnionRule {
  readonly category: 'union'
  readonly variants: readonly Rule[]
}

The variants property must be non-empty.

Intersection Rules

By using the & syntax, you can require that two different rules must be matched.

validator`{ x: number } & { y: number }`
  .assertMatches({ x: 2, y: 3 }); // ✓

validator`{ x: number } & { y: number }`
  .assertMatches({ x: 2 }); // ✕

You'll find this especially useful in scenarios where you are...

  • Composing validators together, and wish to tack on extra rules, e.g. validator`${anotherValidatorInstance} & { x: number }`

  • Wishing to mix custom-made expectations with built-in rule syntax, e.g. validator`number[] & ${expectNonEmptyArray}`

  • etc

Rule Structure

// The `Rule` interface represents any rule
// documented on this page.

interface IntersectionRule {
  readonly category: 'intersection'
  readonly variants: readonly Rule[]
}

The variants property must be non-empty.

Iterable Rules

Special syntax to match against the contents of an iterable is provided as a convenience. This isn't natively supported by TypeScript, but was added because it helps deal with the fact that generic syntax can't be supported in Moat Maker the same way it can in TypeScript. Visit the dedicated generic syntax replacements page to learn more about using the iterable-matching syntax, as well as other generic-replacement tools.

// Iterates over the map instance and ensures each
// yielded entry matches the `[string, number]` pattern,
// thus verifying that this maps strings to numbers.
const myMap = new Map([['a', 1], ['b', 2]]);
validator`${Map}@<[string, number]>`
  .assertMatches(myMap);

Rule Structure

// The `Rule` interface represents any rule
// documented on this page.

interface IterableRule {
  readonly category: 'iterable'
  // The iterable must be this type.
  readonly iterableType: Rule
  // Each yielded value from the iterator must be this type
  readonly entryType: Rule
}

Other Supported Syntax

Parentheses

Parentheses work as you would expect them to work.

validator`(number | string)[]`
  .assertMatches([2, 'x', 3]); // ✓

Comments

Both line comments and block comments are supported.

validator`{
  x: number
  // y: number
  /* z: number */
}`.assertMatches({ x: 3 }); // ✓

Comments will work across interpolated regions, causing the interpolated content to be ignored. (Remember that the interpolated value will still be evaluated, it's just that Moat Maker ignores the received value).

const logAndReturn = () => {
  console.log('Hi There');
  return 5;
};

validator`{
  x: 3
  // y: ${logAndReturn()}
}`.assertMatches({ x: 3 }); // ✓ - "Hi There" is still logged out

Interpolation

A variety of different types of values can be interpolated for different effects. If you interpolate...

  • a primitive, such as a number, string, symbol, etc: These are compared for equality. -0 is considered equal to +0. NaN is also supported, and an interpolated NaN is considered equal to a NaN being validated.

  • Classes/functions: When a class is interpolated, Moat Maker will check that the value being validated is an instance of this class, or an instance of a derived class. Remember that in JavaScript, a class is a function, so there's no practical way to tell them apart. The internal algorithm differs from a normal instanceof check in a couple of ways: 1. The symbol.hasInstance property will be ignored to align more closely with TypeScript's behavior, and 2. when a primitive class is interpolated, brand-checking will be performed, which basically means if you have an instance from one realm (like your browser's parent frame) and a class from another (like an iframe), you can compare the two, and it will work as expected. instanceof does not support cross-realm checks like this. If you wish to check if a value is a direct instance of a class (and not of a derived class), you can use the expectDirectInstance() recipe.

  • Regular Expressions: A value being validated must be a string that conforms to the pattern described in the interpolated regular expression.

  • Certain object types returned by this library, including validator instances, ref objects, and expectation objects: Refer to their individual documentation entries to learn about how they behave when they are interpolated into a validator.

  • Any other object not covered by an above bullet point: These will throw an error. If you wish to compare objects by identity, you can use the expectRef() recipe.

The TypeScript type InterpolatedValue is exported, and represents a value that is allowed to be interpolated into a validator.

Rule Structure

interface InterpolationRule {
  readonly category: 'interpolation'
  // This value corresponds to an index in the ruleset's
  // interpolated array.
  readonly interpolationIndex: number
}

Last updated