Skip to content

A TypeScript module to help construct type assertion functions and type guards.

License

Notifications You must be signed in to change notification settings

antonebbage/typeguardkit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TypeGuardKit

ci codecov deno doc

⚠️ API not yet stable

A TypeScript module to help construct type assertion functions and type guards.

The included classes and functions can be used to create types and their assertion functions and guards from a single source definition.

Type assertion functions and guards can be used to catch incompatible types entering your program due to data corruption, interfaces not being adhered to, or interface definitions being outdated without versioning protection from breaking changes.

Setup

Deno module URL

https://deno.land/x/typeguardkit/mod.ts

npm installation

npm install typeguardkit

Usage

Example imports

Where you see relative import paths in documentation examples, instead import from "https://deno.land/x/typeguardkit/mod.ts" if using Deno, or from "typeguardkit" if using npm.

Example

import {
  _boolean,
  _null,
  _number,
  _PositiveInteger,
  _string,
  array,
  Asserted,
  ObjectAsserter,
  union,
} from "./mod.ts";

// types/book.ts

export const _Book = new ObjectAsserter("Book", {
  isbn: _string,
  title: _string,
  authors: array(_string),
  pageCount: _PositiveInteger,
  rating: union(_number, _null),
  recommended: _boolean,
});

export type Book = Asserted<typeof _Book>;

// api/get_book.ts

export async function getBook(isbn: string): Promise<Book> {
  const response = await fetch(`/api/books/${isbn}`);

  const responseBody = await response.json();

  // If `responseBody` is a `Book`, `_Book.assert` returns `responseBody` as
  // `Book`. Otherwise, `_Book` throws a `TypeAssertionError`, including the
  // optional value name `"responseBody"` in its `message`.

  return _Book.assert(responseBody, "responseBody");
}

Asserter

An Asserter<Type> has a type assertion method, assert, that asserts whether the provided value is of Type.

interface Asserter<Type> {
  readonly typeName: string;

  assert(value: unknown, valueName?: string): Type;
}

If value is of Type, assert should return value as Type. Otherwise, assert should throw a TypeAssertionError, including valueName in its message.

The module includes the _boolean, _number, _string, _null, and _undefined primitive type Asserters.

It also includes the _NonNegativeNumber, _PositiveNumber, _Integer, _NonNegativeInteger, and _PositiveInteger Asserters, which are number Asserters that perform additional validation.

The _null and _undefined Asserters can be used to create union type Asserters with the union function.

As well as wrapping Asserters in the assertIs or is functions, you can use them like this:

import { _string } from "./mod.ts";

function handleUnknown(x: unknown) {
  let y;

  try {
    y = _string.assert(x, "x");
  } catch {
    return;
  }

  // `y` has now been initialized and inferred to be of type `string`, so can be
  // passed to `handleString`. `x` is still of `unknown` type though, so cannot.

  handleString(y);
}

function handleString(y: string) {}

You can create your own Asserters using the TypeAsserter class. For example, the _string Asserter was created like this:

import { TypeAsserter } from "./mod.ts";

export const _string = new TypeAsserter(
  "string",
  (value): value is string => typeof value === "string",
);

Prefixing Asserter names with an underscore _ helps to avoid name conflicts and shadowing.

Assertion signature wrapper

The assertIs function wraps an Asserter<Type> with an assertion signature so the value passed in can be narrowed to Type. If the Asserter throws an error, it will bubble up. Otherwise, assertIs will not return a value, but after calling it, the value passed in will be narrowed to Type.

Example:

import { _string, assertIs } from "./mod.ts";

function handleUnknown(x: unknown) {
  assertIs(_string, x, "x");

  // `x` has now been narrowed to type `string`, so can be passed to
  // `handleString`.

  handleString(x);
}

function handleString(x: string) {}

Predicate signature wrapper

The is function wraps an Asserter<Type> with a predicate signature, creating a type guard, so the value passed in can be narrowed to Type. If the Asserter throws an error, is will catch it and return false. Otherwise, is will return true.

Example:

import { _string, is } from "./mod.ts";

function handleUnknown(x: unknown) {
  if (is(_string, x)) {
    // `x` has now been narrowed to type `string`, so can be passed to
    // `handleString`.

    handleString(x);
  }
}

function handleString(x: string) {}

NumberAsserter

A NumberAsserter is an Asserter<number>, with any additional validation defined by its NumberAsserterOptions properties.

The provided NumberAsserterOptions are made accessible as properties of the created NumberAsserter.

Example:

import { Asserted, NumberAsserter } from "./mod.ts";

export const _EvenNumberInRange = new NumberAsserter("EvenNumberInRange", {
  min: { value: 0, inclusive: true },
  max: { value: 100, inclusive: true },
  step: 2,
});

export type EvenNumberInRange = Asserted<typeof _EvenNumberInRange>;

StringAsserter

A StringAsserter is an Asserter<string>, with any additional validation defined by its StringAsserterOptions properties.

The provided StringAsserterOptions are made accessible as properties of the created StringAsserter.

Example:

import { Asserted, StringAsserter } from "./mod.ts";

export const _NonEmptyString = new StringAsserter("NonEmptyString", {
  minLength: 1,
});

export type NonEmptyString = Asserted<typeof _NonEmptyString>;

export const _NumericString = new StringAsserter("NumericString", {
  regex: { pattern: "\\d+", requirements: ["must be numeric"] },
});

export type NumericString = Asserted<typeof _NumericString>;

export const _Palindrome = new StringAsserter("Palindrome", {
  rules: [
    {
      validate(value) {
        if (value.length < 2) {
          return true;
        }

        const forwardValue = value.replace(/[^0-9a-z]/gi, "");
        const backwardValue = forwardValue.split("").reverse().join("");

        return forwardValue === backwardValue;
      },

      requirements: ["must be a palindrome"],
    },
  ],
});

export type Palindrome = Asserted<typeof _Palindrome>;

LiteralUnionAsserter

A LiteralUnionAsserter is an Asserter for the union of its values.

The provided values are made accessible as a property of the created LiteralUnionAsserter.

Example:

import { Asserted, LiteralUnionAsserter } from "./mod.ts";

export const _Direction = new LiteralUnionAsserter(
  "Direction",
  ["up", "right", "down", "left"],
);

export type Direction = Asserted<typeof _Direction>;

EnumAsserter

An EnumAsserter is an Asserter for the union of the member types of the provided enumObject.

The provided enumObject is made accessible as a property of the created EnumAsserter.

Example:

import { EnumAsserter } from "./mod.ts";

export enum Direction {
  Up,
  Right,
  Down,
  Left,
}

export const _Direction = new EnumAsserter("Direction", Direction);

UnionAsserter

A UnionAsserter is an Asserter for the union of the Types of its memberAsserters.

The provided memberAsserters are made accessible as a property of the created UnionAsserter.

Example:

import { _null, _string, Asserted, UnionAsserter } from "./mod.ts";

export const _stringOrNull = new UnionAsserter(
  "stringOrNull",
  [_string, _null],
);

export type stringOrNull = Asserted<typeof _stringOrNull>;

union

The union function can be used to create a UnionAsserter without specifying a typeName.

Example:

import { _null, _string, Asserted, union } from "./mod.ts";

export const _stringOrNull = union(_string, _null);

export type stringOrNull = Asserted<typeof _stringOrNull>;

ArrayAsserter

An ArrayAsserter is an Asserter for the Array type defined by its elementAsserter, with any additional validation defined by its ArrayAsserterOptions properties.

The provided memberAsserter and ArrayAsserterOptions are made accessible as properties of the created ArrayAsserter.

Example:

import { _number, _string, ArrayAsserter, Asserted } from "./mod.ts";

export const _NonEmptyArrayOfString = new ArrayAsserter(
  "NonEmptyArrayOfString",
  _string,
  { minLength: 1 },
);

export type NonEmptyArrayOfString = Asserted<typeof _NonEmptyArrayOfString>;

export const _ArraySetOfString = new ArrayAsserter(
  "ArraySetOfString",
  _string,
  { mustBeASet: true },
);

export type ArraySetOfString = Asserted<typeof _ArraySetOfString>;

export const _AscendingArrayOfNumber = new ArrayAsserter(
  "AscendingArrayOfNumber",
  _number,
  {
    rules: [
      {
        validate(value) {
          for (let i = 1; i < value.length; i++) {
            if (value[i - 1] > value[i]) {
              return false;
            }
          }

          return true;
        },

        requirements: ["must be in ascending order"],
      },
    ],
  },
);

export type AscendingArrayOfNumber = Asserted<typeof _AscendingArrayOfNumber>;

array

The array function can be used to create an ArrayAsserter without specifying a typeName or ArrayAsserterOptions.

Example:

import { _string, array, Asserted } from "./mod.ts";

export const _ArrayOfString = array(_string);

export type ArrayOfString = Asserted<typeof _ArrayOfString>;

RecordAsserter

A RecordAsserter is an Asserter for the Record type defined by its keyAsserter and valueAsserter.

The provided keyAsserter and valueAsserter are made accessible as properties of the created RecordAsserter.

Example:

import { _string, Asserted, RecordAsserter } from "./mod.ts";

export const _RecordOfStringByString = new RecordAsserter(
  "RecordOfStringByString",
  [_string, _string],
);

export type RecordOfStringByString = Asserted<typeof _RecordOfStringByString>;

record

The record function can be used to create a RecordAsserter without specifying a typeName.

Example:

import { _string, Asserted, record } from "./mod.ts";

export const _RecordOfStringByString = record(_string, _string);

export type RecordOfStringByString = Asserted<typeof _RecordOfStringByString>;

ObjectAsserter

An ObjectAsserter is an Asserter for the object type defined by its propertyAsserters.

The provided propertyAsserters are made accessible as a property of the created ObjectAsserter.

Example:

import { _string, Asserted, ObjectAsserter, option } from "./mod.ts";

export const _User = new ObjectAsserter("User", {
  name: _string,
  emailAddress: option(_string),
});

export type User = Asserted<typeof _User>;

ObjectIntersectionAsserter

An ObjectIntersectionAsserter is an ObjectAsserter for the intersection of the asserted types of the provided ObjectAsserters.

Example:

import {
  _string,
  Asserted,
  ObjectAsserter,
  ObjectIntersectionAsserter,
} from "./mod.ts";

// types/entity.ts

export const _Entity = new ObjectAsserter("Entity", {
  id: _string,
});

export type Entity = Asserted<typeof _Entity>;

// types/user.ts

export const _User = new ObjectIntersectionAsserter(
  "User",
  [
    _Entity,

    {
      name: _string,
    },
  ],
);

export type User = Asserted<typeof _User>;

objectIntersection

The objectIntersection function can be used to create an ObjectIntersectionAsserter without specifying a typeName.

Example:

import {
  _string,
  Asserted,
  ObjectAsserter,
  objectIntersection,
} from "./mod.ts";

// types/a.ts

export const _A = new ObjectAsserter("A", {
  a: _string,
});

export type A = Asserted<typeof _A>;

// types/b.ts

export const _B = new ObjectAsserter("B", {
  b: _string,
});

export type B = Asserted<typeof _B>;

// types/c.ts

export const _C = objectIntersection(_A, _B);

export type C = Asserted<typeof _C>;

PartialAsserter

A PartialAsserter is an ObjectAsserter for the asserted type of the provided ObjectAsserter with all properties set to optional.

Example:

import { _string, Asserted, ObjectAsserter, PartialAsserter } from "./mod.ts";

export const _Options = new PartialAsserter(
  "Options",
  {
    option1: _string,
    option2: _string,
    option3: _string,
  },
);

export type Options = Asserted<typeof _Options>;

partial

The partial function can be used to create a PartialAsserter without specifying a typeName.

Example:

import { _string, Asserted, ObjectAsserter, partial } from "./mod.ts";

// types/user_name.ts

export const _UserName = new ObjectAsserter("UserName", {
  firstName: _string,
  lastName: _string,
});

export type UserName = Asserted<typeof _UserName>;

// types/user_name_update.ts

export const _UserNameUpdate = partial(_UserName);

export type UserNameUpdate = Asserted<typeof _UserNameUpdate>;

PickAsserter

A PickAsserter is an ObjectAsserter for the type constructed by picking the set of properties Keys from the asserted type of the provided ObjectAsserter.

Example:

import { _string, Asserted, ObjectAsserter, PickAsserter } from "./mod.ts";

// types/user.ts

export const _User = new ObjectAsserter("User", {
  id: _string,
  firstName: _string,
  lastName: _string,
});

export type User = Asserted<typeof _User>;

// types/user_name.ts

export const _UserName = new PickAsserter(
  "UserName",
  _User,
  ["firstName", "lastName"],
);

export type UserName = Asserted<typeof _UserName>;

pick

The pick function can be used to create a PickAsserter without specifying a typeName.

Example:

import { _string, Asserted, ObjectAsserter, pick } from "./mod.ts";

// types/user.ts

export const _User = new ObjectAsserter("User", {
  id: _string,
  firstName: _string,
  lastName: _string,
});

export type User = Asserted<typeof _User>;

// types/user_name.ts

export const _UserName = pick(_User, ["firstName", "lastName"]);

export type UserName = Asserted<typeof _UserName>;

The TypeAssertionError issue tree

A TypeAssertionError has an issueTree property of type TypeAssertionIssueTree, which provides a tree representation of the issue data contained in the error's message.

TypeAssertionIssueTree is an alias of TypeAssertionIssueTreeNode for typing the root TypeAssertionIssueTreeNode.

A TypeAssertionError also has an issueTreeNode method to find the node at a given path. The path separator is a forward slash, and a path segment can be an object property name or array index.

issueTreeNode could be called to get the issue data for each field in a form, for example:

import {
  _NonNegativeInteger,
  _string,
  Asserted,
  assertIs,
  is,
  ObjectAsserter,
  TypeAssertionError,
} from "./mod.ts";

// types/item.ts

export const _Item = new ObjectAsserter("Item", {
  quantity: _NonNegativeInteger,
});

export type Item = Asserted<typeof _Item>;

// types/form.ts

export const _Form = new ObjectAsserter("Form", {
  item: _Item,
});

export type Form = Asserted<typeof _Form>;

// elsewhere.ts

const form: Form = {
  item: {
    quantity: 0,
  },
};

let itemQuantityIssues: string[] = [];

function validateForm(): boolean {
  try {
    assertIs(_Form, form);
  } catch (error) {
    if (error instanceof TypeAssertionError) {
      const node = error.issueTreeNode("item/quantity");

      itemQuantityIssues = node?.issues
        ? node.issues.filter((issue) => is(_string, issue)) as string[]
        : [];
    }

    return false;
  }

  return true;
}

Sending TypeAssertionErrors to another program

A TypeAssertionError's toJSON method returns a JSON representation of the error's issueTree, which can then be sent to another program and used to create a new TypeAssertionError there with the same issueTree.

In the receiving program, you can assert that a received object is a TypeAssertionIssueTree using the _TypeAssertionIssueTree Asserter. You can then create a TypeAssertionError by passing the asserted TypeAssertionIssueTree to its constructor.

About

A TypeScript module to help construct type assertion functions and type guards.

Resources

License

Stars

Watchers

Forks