Use Generics

What tools are available to replicate TypeScript's generics?

While there isn't a sensible way to directly provide the behavior of TypeScript's generics at runtime, there are various tools and patterns which can be used to replicate different aspects of generics.

Iterable Matching Syntax

TypeScript generics are often used on data structures. Luckily, such data structures will usually implement the iterator protocol to reveal their contents. Knowing this, Moat Maker provides an iterator-matching syntax, which allows you to verify that every value yielded by an iterator matches a particular pattern.

// Matches against an instance of Set that holds
// only strings.
const mySet = new Set(['a', 'b', 'c']);
validator`${Set}@<string>`
  .assertMatches(mySet);

// Matches against an instance of Map that maps
// strings to coordinates.
const myMap = new Map([['a', { x: 2, y: 3 }]]);
validator`${Map}@<[string, { x: number, y: number }]>`
  .assertMatches(myMap);

// Matches any iterable that yields booleans or numbers
const myArray = [true, false, 2, false];
validator`unknown@<boolean | number>`
  .assertMatches(myArray);

The @<...> iterable-matching syntax is intended to look similar to TypeScript's generic syntax, because it happens to be able to handle a number of similar cases. But, it's also slightly different (it has the extra @) to remind us that it operates in a fundamentally different way.

Validator Factories

Iterable matching can't handle all problems. Some objects might contain data of a particular type, but they might not implement an iterator protocol for various reasons.

Imagine, for example, a generic coordinate type whose values can be of type number, bigint, or whatever you want them to be.

interface Coordinate<T> {
  x: T
  y: T
}

To create a similar schema definition in Moat Maker, it's recommended to create a "validator factory function", which would allow you to configure how the validator behaves via the parameters it receives.

function createCoordinateValidator(type: Validator) {
  return validator`{
    x: ${type}
    y: ${type}
  }`;
}

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

Just how in TypeScript you can pass in any type into the generic Coordinate interface to control the type of each field, you can pass any validator instance into createCoordinateValidator() to control how each field is validated.

validator.from()

To remove some boilerplate of a validator factory, a helper validator.from() function is provided that can take, as an input, either a string or a validator instance, and will always return a validator. If used against the arguments of your validator factory, you enable users of your factory to pass in a string instead of a validator instance, which can be more convenient.

function createCoordinateValidator(type_: string | Validator) {
  const type = validator.from(type_);
  return validator`{
    x: ${type}
    y: ${type}
  }`;
}

// Both of these have the same effect.

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

createCoordinateValidator('number')
  .assertMatches({ x: 2, y: 3 });

This is especially useful when you want to compose validator factories inside another validator, without being overly verbose.

const lineValidator = validator`{
  start: ${createCoordinateValidator('number')}
  end: ${createCoordinateValidator('number')}
}`;

Security note: Do not call validator.from() with untrusted, user-supplied input. We don't want end-users crafting malicious strings that cause the validation algorithm to grind to a halt. It's unlikely that such strings could be built in the current version, but there's no guarantee that features won't be added in the future that enable attacks like these.

Performance note: Generally, to improve performance, Moat Maker's language parser will cache the result of parsing a particular template-string literal. This allows you to put a validator definition inside a repeatedly-used function without worrying about the same text being re-parsed over and over.validator.from() is unique in that it's possible to repeatedly call it with various, dynamically-generated strings, which could quickly cause a cache to fill up and unnecessarily hog memory. Because of this, caching is turned off for string inputs for validator.from(). In most cases, the performance difference shouldn't be noticeable, especially if you're only providing simple inputs, but it's still good to be aware of this behavior.

Transformers

TypeScript provides a number of utility generic types, which fulfill the role of "type transformers". For example, the Partial<> type, which turns all required fields into optional ones. Moat Maker also supports something along this vein, which you can learn about on its dedicated page.

Last updated