vee-type-safe

Simple TypeScript type checking utility library.

Usage no npm install needed!

<script type="module">
  import veeTypeSafe from 'https://cdn.skypack.dev/vee-type-safe';
</script>

README

vee-type-safe

npm version Build Status TypeScript

This is a simple TypeScript type checking utility library. Requires Typescript version >= 3.2.

View detailed API documentation generated by TypeDoc

Quick API glance

mismatch(suspect: unknown, typeDescr: TypeDescription)

Returns null or a MismatchInfo object that stores information about type incompatability with the given TypeDescription, e.g. why and where suspect's invalid property is. This is a powerful tool to generate useful error messages while validating value shape type. Note: this function doesn't allow suspect to have properties not listed in typeDescr which differentiates it from duckMismatch() (see bellow).

    import * as Vts from 'vee-type-safe';
    import { Model } from '@models/model';

    const untrustedJson: unknown = /* ... */;
    const ExpectedJsonTD: Vts.TypeDescription = /* this is actually a generic type (advanced topic) */;
    const dbDocument: Model = /* Some object */;

    const mismatchInfo = Vts.mismatch(untrustedJson, ExpectedJsonTD);
    if (mismatchInfo != null) {
        console.log(
            mismatchInfo.path,
            mismatchInfo.actualValue,
            mismatchInfo.expectedTd
        );
        // logs human readable path to invalid property
        console.log(mismatchInfo.pathString());

        // mismatchInfo.toErrorString() generates human readable error message
        throw new Vts.TypeMismatchError(mismatchInfo);
    }
    // now you may safely assign untrustedJson to dbDocument:
    Object.assign(dbDocument, untrustedJson);

duckMismatch(suspect, typeDescr)

Works the same way as mismatch(suspect, typeDescr) but allows suspect object with excess properties to pass the match.

    import * as Vts from 'vee-type-safe';

    Vts.duckMismatch(
        { name: 'Ihor', somePropertyIDontCareAbout: 42 },
        { name: 'string' }
    ); // returns null as suspect is allowed to have excess properties

    const untrustedJson = {
        client: 'John Doe',
        walletNumber: null,
    };

    const ExpectedJsonTD = Vts.td({ // this noop call is needed to preserve unit types
        client: 'string',
        walletNumber: /\d{16}/ // implies a string of the given format
    });

    // Here we map the given type description shape to the type that it describes statically
    type ExpectedJson = Vts.TypeDescriptionTarget<typeof ExpectedJsonTD>;

    /* 
        ExpectedJson === {
            client:       string;
            walletNumber: string;
        }
    */


    const mismatchInfo = Vts.duckMismatch(untrustedJson, ExpectedJsonTD);
    if (mismatchInfo != null) {
        throw new Vts.TypeMismatchError(mismatchInfo);
    }
    // ^~~~ Vts.ensureDuckMatch() does the same
 

    const trustedJson = untrustedJson as ExpectedJson;
    // process client

What is TypeDescription?

Type description is a simple JavaScript object with values of TypeDescription type or basic typename string ('string', 'number', 'function'...) or Set<TypeDescription> or TypeDescription[] or RegExp or your custom TypePredicate function. TypeDescription is actually a conditional (dependent on type argument) union type of all of these.

Here is an example of how you may describe your type.

import * as Vts from 'vee-type-safe';

Vts.conforms(
{
       prop: 'lala',
       tel:  '8800-555-35-35'
       prop2: true,
       obj: {
           obj: [23, false]
       },
       someIDontCareProperty: null // excess properties are ok for confroms()
},
{
       prop: 'string',
       tel:  /\d{4}-\d{3}-\d{2}-\d{2}/, // claims a string of given format
       prop2: 'boolean',
       obj: {
           obj: ['number', 'boolean'] // claims a fixed length tuple
       }
}); // true

Vts.conforms(
{
     arr: ['array', null, 'of any type', 8888 ],
     strArr: ['Pinkie', 'Promise', 'some', 'strings'],
     oneOf: 2,
     custom: 43
}, 
{
     arr: [],                              // claims an array of any type
     strArr: ['string'],                   // claims an array of any length
     oneOf: new Set(['boolean', 'number']),// claims to be one of these types
     custom: isOddNumber                   // custom type predicate function
}); // true

function isOddNumber(suspect: unknown): suspect is number {
    return typeof suspect === 'number' && suspect % 2;
}  


const HumanTD = Vts.td({  // noop function that preserves unit types
    name: 'string',
    id:   'number'
});

// generate static TypeScript type:
type Human = Vts.TypeDescriptionTarget<typeof HumanTD>;

// type Human === {              
//     name: string;
//     id:   number;
// }

function tryUseHuman(maybeHuman: unknown) {
    if (conforms(maybeHuman, HumanTD)) {
        // maybeHuman is of type that is assignable to Human here
        // it is inferred to be Vts.TypeDescriptionTarget<typeof HumanTD> exactly
        maybeHuman.name;
        maybeHuman.id;
    }
 }

Here is an actual algorithm how conforms() function interprets TypeDescription.

  • If it is a basic JavaScript typename string (should satisfy typeof operator domain definition), then function returns typeof suspect === typeDescr.
  • If it is a RegExp, then returns typeof suspect === 'string' && typeDescr.test(suspect).
  • If it is a Set<TypeDescription>, returns true if suspect conforms to at least one of the given TDs in Set.
  • If it is an Array<TypeDescription> and it consists of one item, returns true if suspect is Array and each of its items conforms to the given TD at typeDescr[0].
  • If it is an Array<TypeDescription> and it consists of more than one item, returns true if suspect is Array and suspect.length === typeDescr.length and each corresponding suspect[i] conforms to typeDescr[i] type description.
  • If it is an empty Array, returns true if suspect is Array of any type.
  • If it is an object, returns true if suspect is also an object and each typeDescr[key] is a TD for suspect[key]. Excess properties in suspect do not matter for conforms() function, but matter for exactlyConforms() and mismatch() functions.
  • If it is a TypePredicate (i.e. (suspect: unknown) => boolean), then returns typeDescr(suspect).

Predefined TypeDescriptions

There are factory functions that return TypeDescriptions (those are often TypePredicates) or already defined TypePredicates, that you should use as type descriptions when calling mismatch/duckMismatch/conforms/exactlyConforms(suspect, typeDescr). TypePredicate is a function of type:

(suspect: unknown) => boolean

If you specify a generic argument TTarget it becomes a true TypeScript type predicate, so that you will be able to get described type from it when using Vts.TypeDescriptionTarget:

(suspect: unknown) => suspect is TTarget

isNumberWithinRange(min, max)

Returns a predicate that returns true if its argument is a number within the range [min, max] or [max, min] if min > max.

import * as Vts from 'vee-type-safe';

Vts.conforms(
{
    num: 32
},
{
    num: Vts.isNumberWithinRange(0, 5)
}); // false

isIntegerWithinRange(min, max)

The same as isNumberWithinRange(min, max), but its returned predicate returns false if forwarded argument is not an integer.

optional(typeDescr: TypeDescription)

Retuns Set(['undefined', typeDescr]))

import * as Vts from 'vee-type-safe';
Vts.conforms(
{
    prop: 'str'
},{
    prop: Vts.optional('number')
}) 
// return false because the property is not undefined, 
// but doesn't conform to 'number' type
Vts.conforms(
{
    prop: -23
},{
    prop: Vts.optional(Vts.isNegativeInteger)
});
// returns true because the property is not undefined
// and conforms to isNegativeInteger restriction
Vts.conforms(
{

},{
    prop: Vts.optional(Vts.isNegativeInteger)
});
// returns true because property 'prop' may be absent

Self explanatory functions

All these functions take unknown type argument and return suspect is number, which is useful as a type guard or when using as a type description.

  • isInteger(suspect)
  • isPositiveInteger(suspect)
  • isNegativeInteger(suspect)
  • isPositiveNumber(suspect)
  • isNegativeNumber(suspect)
  • isZeroOrPositiveInteger(suspect)
  • isZeroOrNegativeInteger(suspect)
  • isZeroOrPositiveNumber(suspect)
  • isZeroOrNegativeNumber(suspect)
  • ...
import * as Vts from 'vee-type-safe';
Vts.conforms(
{
    id: 2,
    volume: 22.5
},
{
    id:    Vts.isPositiveInteger,
    money: Vts.isZeroOrPositiveNumber
}); // true

isOneOf<T>(possibleValues: T[])

Returns a predicate that accepts a suspect of any type and matches it to one of the provided possible values by possibleValues.includes(suspect). Don't confuse it with new Set(possibleValues) when forwarding as a type description to conforms() function, because possibleValues are not TDs, but values to match with.

import * as Vts from 'vee-type-safe';

Vts.conforms(2, Vts.isOneOf([0, 1, 2, 3])); // true
Vts.conforms(2,     new Set([0, 1, 2, 3])); // compile error
// Set<numbers> is not a Set<TypeDescritpion>

Convenient type definitions

interface BasicObject<T>

A shorthand for { [key: string]: T; } type.

type PrimitiveType

A union of all primitive types (null is treated as a primitive type).

type BasicTypeName

A union type of string literals which are in typeof operator domain definition ('string' | 'boolean' | 'object' ...).

vee-type-safe/express (BETA)

This is a library for ExpressJS routing middleware functions.

ensureTypeMatch(getRequestProperty, typeDescr, makeError?)

Returns express.Handler that exactly matches the value returned by getRequestProperty(req) to typeDescr and if it fails, calls next(makeError(failedTypeInfo)). Thus you can be sure that the property of express.Request object was type checked before using it in your middleware.

Does type matching via core library mismatch() function.

  • getRequestProperty: (req: express.Request) => unknown - this function must return a suspect to match to typeDescr, based on the given req argument.
  • typeDescr - type description that the value returned by getRequestProperty(req) will be checked to match to
  • makeError?: (failInfo: MismatchInfo) => unknown - it is an optional function which makes a custom error to forward to next(), by default this function retuns BadTypeStatusError

BadTypeStatusError is an instance of TypeMismatchError that has a status: number property, which is http BAD_REQUEST by default.

    import * as express from 'express';
    import * as VtsEx   from 'vee-type-safe/express'
    import * as Vts     from 'vee-type-safe';
    const router = express.Router();
    interface MessagesPostRequest {
        filters: string[];
        limit: number;
    }

    router.post('api/v1/messages',
        VtsEx.matchType(
            VtsEx.ReqBody, // or req => req.body (your custom obtaining logic here)
            {
                filters: ['string'],
                limit:   Vts.isPositiveInteger
            },
            mmInfo => new MyCustomError(mmInfo.path, mmInfo.actualValue)
        ),
            // replaces standard express.Request.body type with MessagesPostRequest
        (req: VtsEx.ReqBody<MessagesPostRequest>, res, next) => {
            /* your middleware, where you can trust to req.body */
            // req.body has MessagesPostRequest type here
            const filters = req.body.filters.join();
            // ...
        }
    );

There is a list of handy functions to specify as getRequestProperty argument:

  • ReqBody(req) => req.body
  • ReqParams(req) => req.params
  • ReqQuery(req) => req.query
  • ReqCookies(req) => req.cookies
  • ReqHeaders(req) => req.headers
    import * as VtsEx from 'vee-type-safe/express';
    /* ... */
    router.get('api/v1/users/',
        VtsEx.matchType(VtsEx.ReqQuery, { title: 'string' }),
        (req: VtsEx.ReqQuery<{title: string}>, res, next) => {
            const title: string = req.query.title; // now you are sure
            /* ... */
        }
    );