1.0.2 • Published 4 years ago

typeshield v1.0.2

Weekly downloads
2
License
ISC
Repository
github
Last release
4 years ago

TypeShield

TypeShield is a collection of composable TypeScript/JavaScript type guards and assertions. Assertions use the new assertion function types in TypeScript 3.7.

Table of Contents

Installation

TypeShield can be installed like any other npm package.

npm install typeshield

It can then be imported into TypeScript or ES6 JavaScript (with Webpack or Rollup):

import { ... } from 'typeshield';

It can also be used in plain Node.js:

const { ... } = require('typeshield');

or

const typeshield = require('typeshield');

Guards

A type guard in TypeScript is a function that checks the type of a value. This is a function of the form:

function (value: unknown): value is Foo {
    // Perform logic to determine if the value is indeed a 'Foo' and return true if it is, false otherwise.
}

TypeShield exposes the [Guard] type to capture this. It also exposes a more general [Validator] type, which is just a function that takes an unknown value and returns a boolean.

Prebuilt Guards

TypeShield includes a large number of prebuilt guards. A full list can be found in the Guard Reference. An example:

import { isDate } from 'typeshield';

function doSomething(value: unknown) {
    if (isDate(value)) {
        // Inside this block value has type 'Date'
        // ...
    }
}

Some TypeShield functions are factories that require specifying an additional value in order to generate a guard. For example:

import { isStringContaining } from 'typeshield';

function doSomething(value: unknown) {
    if (isStringContaining('foo')(value)) {
        // Inside this block value has type 'string'
        // We also know it contains the substring 'foo' but that isn't known in the type system.
        // ...
    }
}

// You can also save the guard to call for later
const isStringContainingFoo = isStringContaining('foo');
function doSomethingElse(value: unknown) {
    if (isStringContainingFoo(value)) {
        // ...
    }
}

Some guards return tagged primitive types. These are primitive types that are combined with a special tag type that can restrict usage. For example, the [isInteger] guard will type a variable as an [Integer], which is just a number with an additional tag indicating that we know it is an integer.

import { isInteger, Integer } from 'typeshield'

function doSomethingWithNumber(value: number) {
    // ...
}

function doSomethingWithInteger(value: Integer) {
    // ...
}

const foo: number = 5; // Type is 'number'
doSomethingWithNumber(foo); // OK
doSomethingWithInteger(foo); // Error! Argument of type 'number' is not assignable to parameter of type 'Integer'.

if (isInteger(foo)) { // Type is now 'Integer'
    doSomethingWithNumber(foo); // Still OK as 'Integer' is also a 'number'
    doSomethingWithInteger(foo); // OK
}

Comparisons

There are several guards used to compare values with others. The correct one to use depends on your use case:

  • [isIdenticalTo] - Used to compare reference equality using the strict comparison operator (===). Primitives are compared by value but objects are only identical if they have the same reference.
  • [isEqualTo] - Used to compare values. Will first check if objects are identical before falling back to check using the [Equatable] or [Comparable] interfaces. These interfaces allow you to define custom value comparisons.
  • [isDeepEqualTo] - Recursively compares object values using the deep-equal package.
  • [isGreaterThan], [isGreaterThanOrEqualTo], [isLessThan], [isLessThanOrEqualTo] - Compares objects for ordering using the [Comparable] interface or the comparison operators if not implemented. (Note that using >= or <= on objects that do not implement custom valueOf or toString methods will always result in true as their values are compared by string and have the same string representation.)

Here is an example of a custom class implementing [Equatable] and [Comparable]:

import { Comparable, ComparisonResult, Equatable, isGreaterThan, isInstanceOf } from 'typeshield';

class Duration implements Equatable, Comparable {
    public hours: number = 0;
    public seconds: number = 0;

    public constructor(hours: number, seconds: number) {
        this.hours = hours;
        this.seconds = seconds;
    }

    public getTotalSeconds(): number {
        return this.hours * 60 + this.seconds;
    }

    public equals(other: unknown): boolean {
        if (!isInstanceOf(Duration)(other)) return false;
        return this.getTotalSeconds() === other.getTotalSeconds();
    }

    public compareTo(other: unknown): ComparisonResult {
        if (!isInstanceOf(Duration)(other)) return undefined;
        const thisSeconds = this.getTotalSeconds();
        const otherSeconds = other.getTotalSeconds();
        if (thisSeconds > otherSeconds) return 1;
        if (thisSeconds < otherSeconds) return -1;
        return 0;
    }
}

const isGreaterThanZeroDuration = isGreaterThan(new Duration(0, 0));
isGreaterThanZeroDuration(new Duration(5, 0)); // true
isGreaterThanZeroDuration(new Duration(0, -3)); // false

Arrays

Items in arrays can be checked using the [isEach] guard factory. The guard will first check that the value is an array and will then check each item in the array against the specified guard.

import { isEach, isNumber } from 'typeshield';

const isArrayOfNumbers = isEach(isNumber);

function doSomething(value: unknown) {
    if (isArrayOfNumbers(value)) {
        // Inside this block value has type 'number[]'
        // ...
    }
}

Object Properties

Similarly, you can check the properties of an object using the [hasProperties] guard factory:

import { hasProperties, isString } from 'typeshield';

const hasFooStringProperty = hasProperties({
    foo: isString,
});

function doSomething(value: unknown) {
    if (hasFooStringProperty(value)) {
        // This checks if the value has the property 'foo' and that is a string
        // Can now access value.foo
        // ...
    }
}

In the previous example, the guard verifies that the value has the properties specified and infers the type of the object from the guards. Often, however, you have an interface and you want to verify that some object matches the interface. In this case, you can use the very similar [hasInterface] guard factory. In this case you specify the interface as a type parameter, and you then must specify guards that are consistent with the interface. This makes the guard safe from refactoring as renaming a property or changing its type will result in an error. Here is an example:

import { hasInterface, isString } from 'typeshield';

interface Foo {
    foo: string;
    bar: number[];
}

const isFoo = hasInterface<Foo>('Foo', {
    foo: isString,
    bar: isEach(isNumber),
});

function doSomething(value: unknown) {
    if (isFoo(value)) {
        // Inside this block, value has type 'Foo'
        // ...
    }
}

You can combine these guards to nest as deeply as needed.

Both [hasProperties] and [hasInterface] support a function form to support circular references:

import { Guard, hasInterface, isUndefined, or } from 'typeshield';

interface Parent {
    child?: Child;
}

interface Child {
    parent: Parent;
}

const isParent: Guard<Parent> = hasInterface<Parent>('Parent', () => ({
    child: or([ isUndefined, isChild ]),
}));

const isChild: Guard<Child> = hasInterface<Child>('Child', () => ({
    parent: isParent,
}));

Without the function wrappers this would result in an error as isChild is not defined at the time isParent is being defined.

Composing Guards

TypeShield comes with two operators ([or] and [and]) for easily combining guards together to create new guards. (In fact, many of the guards included in TypeShield are created this way.) The [or] operator can be used to check if a value matches one of two or more guards.

import { isNumber, isString, isUndefined } from 'typeshield';

function doSomething(value: unknown) {
    if (or([ isNumber, isString ])(value)) {
        // Inside this block value has type 'number|string'
        // ...
    }
}

// You can also save the guard to call for later
const isStringOrUndefined = or([ isString, isUndefined ]);
function doSomethingElse(value: unknown) {
    if (isStringOrUndefined(value)) {
        // Inside this block value has type 'string|undefined'
        // ...
    }
}

Simlarly, the [and] operator creates an intersection of types.

import { hasProperties, isBoolean, isString } from 'typeshield';

interface Foo {
    foo: boolean;
}
const isFoo = hasProperties<Foo>({
    foo: isBoolean,
}, 'Foo');

interface Bar {
    bar: number;
}
const isBar = hasProperties<Bar>({
    bar: isNumber,
}, 'Bar');

const isFooAndBar = and([ isFoo, isBar ]);

function doSomething(value: unknown) {
    if (isFooAndBar(value)) {
        // Inside this block value has type 'Foo & Bar' so you can access 'value.foo' and 'value.bar'
    }
}

Assertions

TypeScript introduced type support for assertion functions in release 3.7. TypeShield includes the [assert] function to handle arbitrary assertion conditions. If the condition is met, execution continues, if not an [AssertionError] is thrown. Starting with TypeScript 3.7, the type engine also knows how to restrict the types after these assertions.

import { assert } from 'typeshield';

function doSomething(value: unknown) {
    // The condition argument here is whatever you want.
    assert(typeof value === 'string');

    // Now the type of value is 'string'
    // If the value was not a string an error would be thrown.
}

You can also specify a custom error message if you want to control the error message.

Value Assertions

TypeShield also includes an [assertValue] function if you want to make assertions about the value of a variable. This form of the assertion makes it each to throw meaningful error messages. The first value of the function is any [Guard] or [Validator], the second is the value to test, and the 3rd is an optional name for the variable.

import { assertValue, isString } from 'typeshield';

const myVar: number = 5;
assertValue(isString, myVar, 'myVar'); // throws AssertionError: Expected 'myVar' to be a string but received: 5

This works for your custom guards as well:

import { assertValue } from 'typeshield';

function isFoo(value; unknown): value is Foo {
    // Implement here
} 

const myVar: number = 5;
assertValue(isFoo, myVar); // throws AssertionError: Expected value to match 'isFoo' but received: 5

In this example the error message isn't very friendly. You can make the message more friendly by using the optional 4th parameter. This parameter is an expectation message and it is a sentence fragment that follows 'expected value to ':

assertValue(isFoo, myVar, 'myVar', 'be a Foo'); // throws AssertionError: Expected 'myVar' to be a Foo but received: 5

The downside of this approach is that you need to specify the expectation each time you make a value assertion. There is a better alternative approach. The [Guard] and [Validator] types include an additional optional expectation property that allows you to attach the expectation directly to the function. To use it:

(isFoo as Guard).expectation = 'be a Foo';

// From now on:
assertValue(isFoo, myVar); // throws AssertionError: Expected value to be a Foo but received: 5

Most of the prebuilt guards in TypeShield use this property to set more helpful messages, which is why the example with [isString] above looked good.

Assert Property Decorator

TypeShield also includes an [Assert] decorator that can be used on class properties. This decorator converts a property (static or instance) to a getter/setter with a setter that calls [assertValue]. For example:

import { Assert } from 'typeshield';

class MyClass {
    @Assert(or([ isEmail, isUndefined ]))
    public email?: string;
}

const instance = new MyClass();
instance.email = 'foo'; // throws AssertionError: Expected 'MyClass#email' to be an email address or to be undefined but received: foo

The decorator has the same signature as [assertValue] so the name and expectation can be overridden.

You can also use a function that returns a guard in case you have to model a circular relationshop:

import { Assert, isInstanceOf } from 'typeshield';

class Parent {
    @Assert(() => isInstanceOf(Child))
    public child?: Child;
}

class Child {
    @Assert(() => isInstanceOf(Parent))
    public parent?: Parent;
}

Without the function wrapper there would be an error as Child is not defined when the decorator on Parent is called.

Assert Unreachable

Finally, TypeShield includes an [assertUnreachable] function that will always throw if called. This is useful for protecting code when called from JavaScript (i.e. no type checking) or for ensuring fully discriminated unions to be safe from refactoring.

interface A {
    type: 'a';
}

interface B {
    type: 'b';
}

type All = A | B;

function doSomething(arg: All): string {
    if (arg.type === 'a') {
        return 'A';
    }

    if (arg.type === 'b') {
        return 'B';
    }

    // This could be reached if called from JS or if another type is added to 'All'
    assertUnreachable(); // throws AssertionError: Statement should not be reachable
}

Guard Reference

NameFactory ParamsReturn TypeDescription
[and]validators: T[] | function - An array of guards/validatorsExtractIntersection<T>Create a new guard/validator from an intersection of guards/validators
[hasInterface]interfaceName: string - The interface name to report in the error messagevalidators: InterfaceValidators<T> | function - The property validators (or function that returns them)TCreates a guard that tests if a value implements a specified interface
[hasProperties]validators: T | function - An property validators (or function that returns them)ExtractProperties<T>Creates a guard that tests if a value is an object with properties matching the specified property validators
[isAny]anyGuard that tests if the value is an any value (always true)
[isArray]unknown[]Guard that tests if the value is an array
[isBigInt]bigintGuard that tests if the value is an big integer
[isBoolean]booleanGuard that tests if the value is a boolean
[isComparable][Comparable]Guard that tests if the value implements the [Comparable] interface
[isDate]DateGuard that tests if the value is an instance of Date
[isDeepEqualTo]other: T - The object to compare values tostrict: boolean - True to use strict equality (default) and false to use coercive equalityTCreates a guard that tests if a value is equal to the specified value using the deep-equal package{@link https://www.npmjs.com/package/deep-equal}
[isDefined]NonNullable<T>Guard that tests if the value is not null and not undefined
[isEach]eachGuard: Guard<T> - The guard used for each item in the array.T[]Creates a guard that tests if a value is an array and that each value in the array satisfies the given guard.
[isEmail][Email]Guard that tests if the value is an email address
[isEmptyArray]unknown[]Guard that tests if the value is an empty array
[isEmptyString]""Guard that tests if the value is an empty string
[isEqualTo]other: T - The object to compare values toTCreates a guard that tests if a value is equal to a specified object. Values are compared by identity first and then by using the [Equatable] or [Comparable] interfaces, if implemented.
[isEquatable][Equatable]Guard that tests if the value implements the [Equatable] interface
[isFunction]functionGuard that tests if the value is a function
[isGreaterThan]other: T - The value to compare toTCreates a guard that tests if a value is greater than a specified value. Will first compare using the [Comparable] interface, if implemented and will fall back to operator comparison.
[isGreaterThanOrEqualTo]other: T - The value to compare toTCreates a guard that tests if a value is greater than or equal to a specified value. Will first compare using the [Comparable] interface, if implemented and will fall back to operator comparison. Note that objects not implementing [Comparable] or custom value representations may return unexpected results as JS will revert to comparing string representations.
[isIdenticalTo]other: T - The other value to compare toTCreates a guard that tests if a value is identical to another value. This uses the JS strict equality comparison operator (===) so primitives are compared by value but objects are compared by reference.
[isInstanceOf]constructor: Constructor<T> - The constructorTCreates a guard that tests if a value is an instance of the specified constructor
[isInteger][Integer]Guard that tests if the value is an integer
[isLessThan]other: T - The value to compare toTCreates a guard that tests if a value is less than a specified value. Will first compare using the [Comparable] interface, if implemented and will fall back to operator comparison.
[isLessThanOrEqualTo]other: T - The value to compare toTCreates a guard that tests if a value is less than or equal to a specified value. Will first compare using the [Comparable] interface, if implemented and will fall back to operator comparison. Note that objects not implementing [Comparable] or custom value representations may return unexpected results as JS will revert to comparing string representations.
[isMatch]regexp: RegExp - The regular expressionstringCreates a guard that tests if a value is a string that matches the specified RegExp
[isNegative][Negative]Guard that tests if the value is a negative number
[isNegativeInteger][NegativeInteger]Guard that tests if the value is a negative integer
[isNil]unknownGuard that tests if the value is null or undefined
[isNonEmptyArray]unknown[]Guard that tests if the value is an array that is not empty
[isNonEmptyString]stringGuard that tests if the value is a string that is not empty
[isNull]nullGuard that tests if the value is null
[isNumber]numberGuard that tests if the value is a number
[isObject]objectGuard that tests if the value is any object (and not null)
[isPositive][Positive]Guard that tests if the value is a positive number
[isPositiveInteger][PositiveInteger]Guard that tests if the value is a positive integer
[isString]stringGuard that tests if the value is a string
[isStringContaining]substring: string - The substring to check forstringCreates a guard that tests if a value is a string containing the specified substring
[isStringNotContaining]substring: string - The substring to check forstringCreates a guard that tests if a value is a string that does not contain the specified substring
[isSymbol]symbolGuard that tests if the value is a symbol
[isUndefined]undefinedGuard that tests if the value is undefined
[isUnknown]unknownGuard that tests if the value is an any unknown value (always true)
[or]validators: T[] | function - An array of guards/validators (or function that returns an array)ExtractUnion<T>Create a new guard/validator from a union of guards/validators