I wrote a runtime type checker.

Dec 1, 2023 (3 months ago)

3 minutes

31 views

An article about "type-guarder", a runtime type checker I built for TypeScript.

Why?

TypeScript is great, but it's not perfect. Given enough time, you'll eventually run into a situation where half of your code is type checking. It's not fun, it's annoying, and it's error prone.

I wanted a package that would allow me to write type checking code in a more declarative manner. Many such packages already exist, but I believe you never truly learn something unless you build it youself.

Thus, typr was born.

How does you use it?

typr is a recursive runtime type checker. You define a type and provide a value, and it will ensure that the type you provide, is the type you get.

There are three main functions that you can use to check types: conforms, verify, and force.

conforms(value: any): boolean

This will check if the value passed in conforms to the type. It returns a boolean for whether the value conforms to the type.

verify(value: any): T | undefined

This function will check if the value conforms and if it does, it will return the value with the correct generic types in the type system by parsing out the raw type from the type provided. If it isn't the write type, it will return undefined.

force(value: any): T

This function is almost identical to verify(...) however it will return T instead of T | undefined. If the value does not conform to the type, an error will be thrown.

Usage

This can be simple:

import { T } from "@elijahjcobb/typr";

T.string().verify("Hello World!"); // "Hello World!"
T.string().verify(123); // undefined
T.string().verify(false); // undefined

Or with more complex types:

import { T } from "@elijahjcobb/typr";

T.object({
  name: T.string(),
  isAdmin: T.boolean(),
}).verify({ name: "Elijah", isAdmin: true }); // {name: "Elijah", isAdmin: true}

T.object({
  name: T.string(),
  isAdmin: T.boolean(),
}).verify(3); // undefined

Or with even more complex types:

import { T } from "@elijahjcobb/typr";

const checker = T.object({
  name: T.union(
    T.string(),
    T.object({
      first: T.string(),
      last: T.string(),
    })
  ),
  isAdmin: T.boolean(),
});

checker.verify({
  name: "Elijah",
  isAdmin: true,
}); // {name: "Elijah", isAdmin: true}

checker.verify({
  name: { first: "Elijah", last: "Cobb" },
  isAdmin: true,
}); // {name: {first: "Elijah", last: "Cobb"}, isAdmin: true}

checker.verify({
  name: "Elijah",
  isAdmin: 9,
}); // undefined
How does it work?

typr is set up to be extensible. A base class exists, and all other types extend that base class. The base class has a few helper functions, and requires that all children classes implement a conforms function.

The TType Class

All special types extend the TType class. This class has the implementation for the few helper functions force and verify. It requires that any children classes implement the conforms function.

export abstract class TType<T> {
  protected constructor() {}

  public abstract conforms(value: any): boolean;

  public verify(value: any): T | undefined {
    if (!this.conforms(value)) return undefined;
    return value as unknown as T;
  }

  public force(value: any): T {
    if (!this.conforms(value))
      throw new Error("typr found a type that does not conform");
    return value as unknown as T;
  }
}

Strings