npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@maluscat/runtime-type-check

v1.1.1

Published

Modular runtime type checker with focus on creating readable and smart error messages

Downloads

192

Readme

RuntimeTypeCheck

Minimal, modular type checker for the runtime with a heavy focus on producing readable and smart error messages.

Installation

Since this library is completely runtime agnostic, it can be used inside any JavaScript environment, including the web.

Download

The only required file is RuntimeTypeCheck.js inside the script folder. If you want type checking, fetch RuntimeTypeCheck.d.ts as well!

npm

Available on npm under @maluscat/runtime-type-check. Use your favorite package manager:

yarn add @maluscat/runtime-type-check
bun install @maluscat/runtime-type-check
npm install @maluscat/runtime-type-check

Concepts

The core concept of RuntimeTypeCheck is a Condition, which is a building block that contains assertion information. Conditions can recursively extend other conditions and can be combined "OR" and "AND" wise.

This allows an overarching assertion to be split up into multiple smaller conditions, ensuring flexibility and reusability. This means that conditions are able to focus on only one part of an assertion while safely assuming the passed value to already match various other layers. For example, a condition divisible(n) could extend the condition number and can thus safely assume that any passed value is a number.

So, if we want to assert a value to be a positive number divisible by 5, we can AND-combine two assertions positive and divisible(5), both of which will extend the condition number (see the examples below).

Condition

The TS type of a condition looks like this:

interface Condition {
  /** Assertion function. */
  assert: (value: any) => boolean;
  /**
   * Conditions that this condition relies on.
   * Note that this field is a Descriptor, so an "OR" list of "AND" conditions.
   */
  conditions?: Descriptor;
  /**
   * Description of what the correct type should be.
   *
   * This will be merged with other conditions to form a coherent sentence
   * of the desired type (e.g. "Expected a positive number of length 5").
   */
  shouldBe: Message;
  /**
   * Generic description of any value that does **not** assert,
   * so the opposite of what is asserted for.
   *
   * E.g. "a floating point number" when asserting an integer.
   */
  is: string | ((data: IsData) => string);
}

where

/** Will be merged into a sentence of the form "...before type ...after" */
interface Message {
  /** Will be put before the type (e.g. "positive") */
  before?: string;
  /** A noun (e.g. "integer" or "string") */
  type?: string;
  /** Will be put after the type (e.g. "of length 5") */
  after?: string;
}
interface IsData {
  val: any;
  type: Type;
  article: 'a' | 'an';
}
type Type =
  | 'array' | 'NaN' | 'null' | 'string' | 'number' | 'bigint'
  | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function';

See below for a more detailed overview with examples, and the docs for more in-depth descriptions.

Usage

The only non-typing-related exports are RuntimeTypeCheck (main library), Cond (predefined conditions) and, if needed, TypeCheckError (thrown by assertAndThrow):

import { RuntimeTypeCheck, Cond, TypeCheckError } from '@maluscat/runtime-type-check';

See the docs for an overview of all additional typing related exports for use in TypeScript.

RuntimeTypeCheck is an entirely static class. For most use cases, there are only two relevant methods: assert (returns boolean) and assertAndThrow (throws an explanatory error message when it does not assert).

Parameters

Every method accepts its conditions as a rest parameter, any of which may assert (OR). One parameter can either be a single condition or an AND array of conditions.

Hence, this matches either a string OR a number:

RuntimeTypeCheck.assert(3, Cond.string, Cond.number)

Whereas this matches a positive number:

RuntimeTypeCheck.assert(3, [ Cond.positive, Cond.number ])

This also applies to the conditions parameter of a Condition.

assert(value, ...descriptor)

Returns a boolean of whether the passed value matches the passed descriptor.

assertAndThrow(value, ...descriptor)

If the given value does not assert, assertAndThrow throws an error message that automatically catches the most relevant condition in the context of the given value. So, when asserting, say, either a string or a positive integer against the value -3, the method will explain that the given number may not be negative. See the examples for a more detailed overview.

If you need to modify or catch a potentially thrown error, it is good practice to test the caught error for an instance of TypeCheckError. You can then use its message field as-is or access the two parts of the message: is and expected:

try {
  RuntimeTypeCheck.assertAndThrow(-3, Cond.string, Cond.false);
} catch (err) {
  if (err instanceof TypeCheckError) {
    // message:  "Expected string OR false, got number"
    // expected: "string OR false"
    // is:       "number"
    console.log(err.expected, err.is, err.message);
  } else throw err;
}

Cond

Cond (alias: RuntimeTypeCheck.Cond) pre-defines commonly used conditions. See an overview in the docs.

There are also additional conditions included below that have not made it into the library.

Examples

Using provided Cond

Let's assert a value to be either a positive integer or a string. For this, the conditions provided by the Cond class can be used.

import { RuntimeTypeCheck, Cond } from '@maluscat/runtime-type-check';

// true
RuntimeTypeCheck.assert('foobar', Cond.string, [ Cond.positive, Cond.integer ]);

// false (not positive)
RuntimeTypeCheck.assert(-3, Cond.string, [ Cond.positive, Cond.integer ]);

// TypeCheckError: "Expected positive integer OR string, got a negative number or 0"
RuntimeTypeCheck.assertAndThrow(-3, Cond.string, [ Cond.positive, Cond.integer ]);

Array with inner type

Cond also provides conditions for array and object (both of which are functions!) that can take a descriptor of their inner values:

// true
RuntimeTypeCheck.assertAndThrow([ 'foobar' ], Cond.array(Cond.string))

// TypeCheckError: "Expected Array<string>, got number"
RuntimeTypeCheck.assertAndThrow(5, Cond.array(Cond.string))

Because of the conditions' dynamic nature, this can be nested and combined:

// true for both (Array<string[], number>)
RuntimeTypeCheck.assertAndThrow([['foobar']], Cond.array(Cond.array(Cond.string), Cond.number));
RuntimeTypeCheck.assertAndThrow([69], Cond.array(Cond.array(Cond.string), Cond.number));

// TypeCheckError: "Expected Array<Array<string> OR number>, got number"
RuntimeTypeCheck.assertAndThrow(5, Cond.array(Cond.array(Cond.string), Cond.array));

Defining custom conditions

Now we want to assert a number that's divisible by 5 and is greater than 25. The builtin Cond does not provide any help here, so we can define the required conditions ourselves.

Both conditions should extend Cond.number to be sure that any incoming values are already numbers. We will also take the liberty and make both of them a generic generator:

const divisibleBy = (divisor: number) => ({
  conditions: [ Cond.number ], // Ensure that it's a number
  assert: (val: number) => val % divisor === 0,
  shouldBe: { after: `that is divisible by ${divisor}` },
  is: `a number not divisible by ${divisor}`
} satisfies Condition) as Condition;

const greaterThan = (value: number) => ({
  conditions: [ Cond.number ],
  assert: (val: number) => val > value,
  shouldBe: { after: `that is greater than ${value}` },
  is: `a number less than or equal to ${value}`
} satisfies Condition) as Condition;

We can now instantiate these functions to generate the conditions we want and combine them:

const divisibleBy5 = divisibleBy(5);
const greaterThan25 = greaterThan(25);

const divisibleBy5AndGreaterThan25 = [ divisibleBy5, greaterThan25 ];

Now the condition is usable anywhere.

// true
RuntimeTypeCheck.assertAndThrow(30, divisibleBy5AndGreaterThan25);

/*
* TypeCheckError:
*   Expected number that is divisible by 5 and is greater than 25,
*   got a number less than or equal to 25
*/
RuntimeTypeCheck.assertAndThrow(25, divisibleBy5AndGreaterThan25);

/*
* TypeCheckError:
*   Expected number that is divisible by 5 and is greater than 25,
*   got a number not divisible by 5
*/
RuntimeTypeCheck.assertAndThrow(26, divisibleBy5AndGreaterThan25);

Obviously, this can also be combined with other conditions. If conditions are equally faulty, the first of them contributes its message.

/*
* TypeCheckError:
*   Expected positive number that is divisible by 5 and is greater than 25,
*   got a negative number or 0
*/
RuntimeTypeCheck.assertAndThrow(-6, [ Cond.positive, ...divisibleBy5AndGreaterThan25 ]);

/*
* TypeCheckError:
*   Expected positive number that is divisible by 5 and is greater than 25,
*   got a number not divisible by 5
*/
RuntimeTypeCheck.assertAndThrow(-6, [ divisibleBy5, ...divisibleBy5AndGreaterThan25 ]);

Docs

See the generated docs for a more in-depth overview of the library.

Additional conditions

Here are some useful conditions not provided by the base library that can just be copy pasted into your own code if you need them! This is because RuntimeTypeCheck is to be kept as light weight as possible.

/**
 * Assert a value to be not negative (0 or more).
 * Implies {@link Cond.number}.
 */
const nonnegative = {
  conditions: [ Cond.number ],
  assert: (val: number) => val >= 0,
  shouldBe: { before: 'non-negative' },
  is: 'a negative number'
};
/**
 * Generate a condition that asserts a value to be inside
 * the given interval (inclusive). Implies {@link Cond.number}.
 *
 * @param min Lower interval boundary (inclusive)
 * @param max Upper interval boundary (inclusive)
 */
const range = (min: number, max: number) => ({
  conditions: [ Cond.number ],
  assert: (val: number) => val >= min && val <= max,
  shouldBe: { after: `of the interval [${min}, ${max}]` },
  is: 'a number outside of the required range'
});
/**
 * Generate a condition that asserts a value to be divisible
 * by the given divisor. Implies {@link Cond.number}.
 */
const divisibleBy = (divisor: number) => ({
  conditions: [ Cond.number ], // Ensure that it's a number
  assert: (val: number) => val % divisor === 0,
  shouldBe: { after: `that is divisible by ${divisor}` },
  is: `a number not divisible by ${divisor}`
});
/**
 * Generate a condition that asserts a value to be greater
 * than the given value. Implies {@link Cond.number}.
 */
const greaterThan = (value: number) => ({
  conditions: [ Cond.number ],
  assert: (val: number) => val > value,
  shouldBe: { after: `that is greater than ${value}` },
  is: `a number less than or equal to ${value}`
});

Dev fact

This project incubated within Slider89, with this being the last public point of reference. As the rewrite of this version was finished, I immediately rewrote it again with an even better approach, so the current version is technically the third major iteration.