Home

Awesome

<p align="center"> <a href="https://github.com/vedantroy/typecheck.macro"> <img alt="logo" title="typecheck.macro" src="https://raw.githubusercontent.com/vedantroy/typecheck.macro/master/assets/logo.png" width="300" alt="Logo"> </a> <h4 align="center">Automatically generate 🔥 blazing🔥 fast validators for Typescript types.</h4> </p>

Babel Macro

Example

type Cat<T> = {
    breed: "tabby" | "siamese";
    isNice: boolean
    trinket?: T;
}
registerType('Cat')
// Turned into a validation function at compile time through the power of babel macros
// You can also use createDetailedValidator to get error messages
const isNumberCat = createValidator<Cat<number>>()
isNumberCat({ breed: "tabby", isNice: false })                 // true
isNumberCat({ breed: "corgi", isNice: true, trinket: "toy" })  // false

Purpose

Because Typescript types are erased at compile time you can't use them to validate data at runtime. For example, you might want to ensure an API is returning data that matches a given type at runtime. This library (macro) generates validation functions for your Typescript types at compile time.

Why this macro?

Ease of Use

With typecheck.macro you can write normal Typescript types and automatically get validation functions for them. Other validation libraries require you to write your types in a DSL. Thus, typecheck.macro naturally integrates into your project without requiring you to change any existing code.

typecheck.macro supports a large portion of the Typescript type system (support table) so you can validate most of your existing types automatically.

typecheck.macro has features, such as comprehensive error messages and automatic detection and support of circular types, that other projects do not have.

Performance

typecheck.macro generates specialized validation functions that are pure Javascript at compile time. (Almost) every other library generates generic data structures that are plugged into a generic validator function.

typecheck.macro is up to 3x faster than ajv, the fastest JSON schema validator. And anywhere from 6 to 500 times faster than popular libraries, like runtypes or zod.

typecheck.macro is smart. It will analyze your type and determine the fastest/most minimal validation function that can validate your type. For example, the type "Hello" | string will automatically be simplified to string and the type A in type A = B | number; type B = string | C; type C = string will automatically be simplified to type A = string | number, and the appropriate validation code will be generated.

Installation

If you are using Gatsby or Create React App, you can just install the macro. No other steps needed!

Otherwise, you will need to switch over to compiling your Typescript with Babel. This isn't difficult since Babel has good Typescript support. See the example.

Step by Step Instructions

  1. Install dependencies for compiling Typescript with Babel and using macros. [pnpm|npm|yarn] install --save-dev @babel/core @babel/cli @babel/preset-typescript @babel/plugin-transform-modules-commonjs babel-plugin-macros typecheck.macro
    • @babel/plugin-transform-modules-commonjs is so export and import are turned into module.exports and require, so your code will work in Node.
  2. Add the file babel.config.json to the root of your project with the contents:
{
  "presets": ["@babel/preset-typescript"],
  "plugins": ["babel-plugin-macros", "@babel/plugin-transform-modules-commonjs"]
}
  1. Add the command babel src --out-dir dist --extensions \".ts\" to your "package.json". All typescript files in "src" will be compiled (with the macro enabled) to the dist directory.

Usage

Basic Usage

In addition to reading this, read the example.

import createValidator, { registerType } from 'typecheck.macro'

type A = {index: number, name: string}
registerType('A')
// named type
const validator = createValidator<A>()
// anonymous type
const validator2 = createValidator<{index: number, name: string}>()
// mix and match (anonymous type that contains a named type)
const validator3 = createValidator<{index: number, value: A}>()

registerType(typeName: string)

If you want to validate a named type or an anonymous type that references a named type, you must register the named type.

typeName is the name of the type you want to register. The type declaration must be in the same scope of the call to registerType.

{
    type A = {val: string}
    registerType('A') // registers A
    {
        registerType('A') // does nothing :(
    }
}
registerType('A') // does nothing :(

All registered types are stored in a per-file global namespace. This means any types you want to register in the same file should have different names.

registering a type in one file will not allow it to be accessible in another file. This means you cannot generate validators for multi-file types (a type that references a type imported from another file). If this is a big issue for you, go to the "Caveats" section.

A work-around for supporting multi-file types is to move your multi-file types into one file (so they are no longer multi-file types). Then generate the validation functions in that file and export to them where you want to use them. This works because validation functions are just normal Javascript!

registerType automatically registers all types in the same scope as the original type it is registering that are referred to by the original type.

type A = {val: string}
type B = {val: A}
type C = {val: A}
// registers A and B, but not C, since B only refers to A.
registerType('B')

All instances of the registerType macro are evaluated before any instance of createValidator. So ordering doesn't matter.

Most of the primitive types (string, number, etc.) are already registered for you. As are Array, Map, Set and their readonly equivalents.

createValidator<T>(opts?: BooleanOptions, userFuncs?: UserFunctions): (value: unknown) => value is T

Creates a validator function for the type T.

T can be any valid Typescript type/type expression that is supported by the macro.

At compile time, the call to createValidator will be replaced with the generated code.

BooleanOptions: {circularRefs?: boolean, allowForeignKeys?: boolean}

createDetailedValidator<T>(opts?: DetailedOptions, userFuncs?: UserFunctions)

Full type signature:

function createDetailedValidator<T>(
  opts?: DetailedOptions
): (
  value: unknown,
  errs: Array<[string, unknown, IR.IR | string]>
) => value is T;

Creates a detailed validator function for the type T. Example usage:

const v = createDetailedValidator<{x: string}>()
const errs = []
const result = v({x: 42}, errs) // result is false
// errors = [["input["x"]", 42, "type: string"]]

The resulting validation function takes 2 parameters:

DetailedOptions: BooleanOptions & { expectedValueFormat: string }

Constraints

What if you want to enforce arbitrary constraints at runtime? For example, ensure a number in an interface is always positive. You can do this with constraints. You can enforce an arbitary runtime constraint for any user-defined type (e.g not number, string, etc.).

The type of the 2nd parameter of both createValidator and createDetailedValidator:

type UserFunction = { constraints: { [typeName: string]: Function } }

Context

type NumberContainer = {
  pos: Positive;
}
type Positive = number;

With Boolean Validation

const x = createValidator<NumberContainer>(undefined, {
  constraints: {
    Positive: x => x > 0
  }
})

Notes:

With Detailed Validation

const x = createDetailedValidator<NumberContainer>(undefined, {
  constraints: {
    Positive: x => x > 0 ? null : "expected a positive number"
  }
})

Note: The constraint function returns an error (if there is one). Otherwise it returns a falsy value. Any truthy value will be treated as an error message/object.

Support Tables

See the exec tests to get a good idea of what is supported

Primitives Types

PrimitivesSupport
numberYes
stringYes
booleanYes
objectYes
anyYes
unknownYes
BigIntWIP
SymbolWIP

Builtin Generic Types

TypeSupportNotes
ArrayYes
ReadonlyArrayYesSame as Array at runtime.
MapYes
ReadonlyMapYesSame as Map at runtime.
SetYes
ReadonlySetYesSame as Set at runtime.

Typescript Concepts

Language FeaturesSupportNotes
interfaceYesextending another interface is WIP
type aliasYes
genericsYes
union typesYes
tuple typesYes
arraysYes
index signaturesYes
literal typesYes
circular referencesYes
parenthesis type expressionsYes
intersection typesYesOne caveat (caveats)
Mapped TypesWIP
Multi-file typesiffyRequires CLI tool instead of macro
User-declared classesNo

Performance Table

Notes:

Boolean Validator

LibrarySimpleComplexNotes
typecheck.macro46105
ajv108331
io-ts235
runtypes357
zod11471zod throws an exception upon validation error, which resulted in this extreme case

Error Message Validator

Note: Out of all libraries, typecheck.macro has the most comprehensive error messages!

[Benchmarking is WIP]

Generate data with pnpm run bench:prep -- [simple|complex|complex2] and run a benchmark with pnpm run bench -- [macro|ajv|io-ts|runtypes|zod] --test=[simple|complex|complex2]

Caveats

Contributing

Read the contributor docs. Contributions are welcome and encouraged!