Recipes

Small copy-paste friendly utilities

expectRef()

Moat Maker does not allow you to interpolate an object to compare by identity. If you want this behavior, you have to use a custom expectRef() expectation as follows:

function expectRef(expectedRef: object) {
  return validator.expectTo(value => {
    if (value !== expectedRef) {
      return `be the same object as ${String(expectedRef)}`;
    }
  });
}

// Example usage
const myRef = {};
validator`${expectRef(myRef)}`.assertMatches(myRef); // ✓
validator`${expectRef(myRef)}`.assertMatches({}); // ✕

expectCloseTo()

Comparing floats is dangerous - this includes any decimal number or number with a large magnitude (numbers outside of the Number.MIN_SAFE_INTEGER to Number.MAX_SAFE_INTEGER range). Due to minor rounding errors, they won't always compare when they should, like in this example:

// ✕ - Expected <receivedValue> to be 0.3
//     but got 0.30000000000000004.
validator`0.3`.assertMatches(0.1 + 0.1 + 0.1);

You can use the expectCloseTo() custom expectation to make sure a provided number is close to a target number, without requiring the two numbers to be exactly the same.

function expectCloseTo(target: number, { plusOrMinus }: { plusOrMinus: number }) {
  return validator.expectTo(value => {
    if (typeof value !== 'number' || Math.abs(target - value) > plusOrMinus) {
      return `be equal to ${target}±${plusOrMinus}`;
    }
  });
}

// ✓
validator`${expectCloseTo(0.3, { plusOrMinus: 1e-10 })}`
  .assertMatches(0.1 + 0.1 + 0.1);

expectDirectInstance()

When you interpolate a class, it'll be used to check if a passed-in instance is an instance of that class, or of a derived class.

class MyMap extends Map {}

validator`${Map}`.assertMatches(new Map()); // ✓
validator`${Map}`.assertMatches(new MyMap()); // ✓

This is TypeScript's default behavior, but it might not be the behavior you want. If your end-users don't need the power of providing instances of derived classes to your API, you could choose to actively prevent it. This sort of prevention can be beneficial as it helps avoid internal details of your API from leaking out - think of end-users who override default methods with badly-behaving ones, and then minor updates to your library are released that cause their fragile methods to break. For many scenarios, it's overkill to worry about this sort of thing, but if you do wish to worry, expectDirectInstance() is here to help.

function expectDirectInstance(TargetClass: (new (...params: any) => any)) {
  return validator.expectTo((value: unknown) => {
    const isDirectInstance = Object(value).constructor === TargetClass;
    return isDirectInstance ? undefined : `be a direct instance of ${TargetClass.name}.`;
  });
}

// Example usage
class MyMap extends Map {}
validator`${expectDirectInstance(Map)}`.assertMatches(new Map()); // ✓
validator`${expectDirectInstance(Map)}`.assertMatches(new MyMap()); // ✕

expectNonSparse()

Both the array and tuple validators permit sparse arrays to get through. Unless you specifically design to support sparse arrays, they can be dangerous and cause things to behave in unexpected ways. In most cases, you shouldn't need to worry about this problem (it's not common to see sparse arrays in practice, and in reality, there's never a time they should be used), but if you wish to actively prevent them from crossing your moat, expectNonSparse() can help.

const expectNonSparse = validator.expectTo((array: unknown) => {
  if (!Array.isArray(array)) return null;
  for (let i = 0; i < array.length; i++) {
    if (!(i in array)) {
      return `not be a sparse array. Found a hole at index ${i}.`;
    }
  }
});

// Example usage
validator`${expectNonSparse}`.assertMatches([2, undefined, 3]); // ✓
validator`${expectNonSparse}`.assertMatches([2, , 3]); // ✕

The use of this custom expectation is technically redundant if you're matching against an array/tuple rule that does not permit undefined entries (holes are treated as undefined during validation).

getMatchFailureMessage()

There are cases where you might want the match failure error message, without having to go through the trouble of catching and extracting the error message. The following helper function can be used to achieve this.

interface GetMatchFailureMessageOpts {
  readonly at?: string
  readonly errorPrefix?: string
}

function getMatchFailureMessage(
  validator: Validator,
  valueToValidate: unknown,
  opts: GetMatchFailureMessageOpts = {},
): string | undefined {
  class ValidationError extends Error {}

  try {
    validator.assertMatches(valueToValidate, {
      ...opts,
      errorFactory: (...args) => new ValidationError(...args),
    });
  } catch (error) {
    if (error instanceof ValidationError) {
      return error.message;
    } else {
      throw error;
    }
  }

  return undefined;
}

// Example usage

// Returns 'Expected <receivedValue> to be of type "number" but got type "string".'
getMatchFailureMessage(validator`number`, 'not-a-number');

// Returns 'Something went wrong: Expected <value> to be of type "number" but got type "string".'
getMatchFailureMessage(validator`number`, 'not-a-number', {
  at: '<value>',
  errorPrefix: 'Something went wrong:',
});

// Returned undefined
getMatchFailureMessage(validator`number`, 2);

By default, Moat-Maker will throw an instance of TypeError if the assertion fails. A TypeError can also be thrown for other reasons, for example, if you pass in an invalid "at" or "errorPrefix" parameter into the .assertMatches() function. For this reason, the above code sample tells Moat-Maker to throw an instance of a custom Error class to make sure we're only catching match failures and nothing else.

Last updated