A blazing fast, dependency free, 1kb runtime type-checking library written entirely in typescript






Typed

A blazing fast, dependency free, 1kb runtime type-checking library written entirely in typescript, meant to be used with it.

There are dozens of validation libraries out there, so why create yet another one? Well, I tried almost every library out there and there is only one that I really like called superstruct (which is awesome) that provides almost everything that I want, but still, I wanted to create my own. The others are simply bloated or don’t provide proper typescript support. So that’s where typed comes in.

typed is all about function composition. Each function is “standalone” and provides a safe way to validate data, you don’t need a special kind of function to execute a schema against some value. All functions return a special type which is either Success<T> or Failure. If success is true then value is available and fully typed and if not, errors is available with a message and path from where it failed.

Install

npm install typed

Usage

import * as T from "typed";

const postType = T.object({
  id: T.number,
  title: T.string,
  tags: T.array(T.string),
});

const result = postType(/* some JSON data */);

if (result.success) {
  // value is available inside this block
  result.value;
} else {
  // errors is available inside this other block
  result.errors;
}

Types

typed only ships with a few primitives which serves as building blocks for more complex types.

  • any: Typed<any> (defeats the purpose, don’t use unless necessary)
  • array<T>(type: Typed<T>): Typed<T[]>
  • boolean: Typed<boolean>
  • date: Typed<Date>
  • defaulted<T>(type: Typed<T>, fallback: T): Typed<T>
  • enums<T>(enum: T): Typed<T> (Real typescript enums only)
  • literal(constant: string | number | boolean | null): Typed
  • nullable<T>(type: Typed<T>): Typed<T | null>
  • number: Typed<number>
  • object<T extends Shape>(shape: T): Typed<Infer<T>>
  • optional<T>(type: Typed<T>): Typed<T | undefined>
  • string: Typed<string>
  • tuple(...types: Typed[]): Typed<[...types]>
  • union(...types: Typed[]): Typed<T1 | T2 | ... T3>

Type casting

  • asDate: Typed<Date>
  • asNumber: Typed<number>
  • asString: Typed<string>

As you can see, typed provides a few type-casting methods for convenience.

import * as T from "typed";

const postType = T.object({
  id: T.asNumber,
  createdAt: T.asDate,
});

postType({ id: "1", createdAt: "2021-10-23" }); // => { id: 1, createdAt: Date("2021-10-23T00:00:00.000Z") }

Custom validations

typed allows you to refine types with the map function as you’ll see next.

import * as T from "typed";
import isEmail from "is-email";

const emailType = T.map(T.string, (value) =>
  isEmail(value)
    ? T.success(value)
    : T.failure(T.toError(`Expecting value to be a valid 'email'`)),
);

// Later in your code
const userType = T.object({
  id: T.number,
  name: T.string,
  email: emailType,
});

map also allows you to convert or re-shape an input type to another output type.

import * as T from "typed";

const rangeType = (floor: number, ceiling: number) =>
  T.map(T.number, (value) => {
    if (value < floor || value > ceiling) {
      return T.failure(
        T.toError(
          `Expecting value to be between '${floor}' and '${ceiling}'. Got '${value}'`,
        ),
      );
    }
    return T.success(value);
  });

const latType = rangeType(-90, 90);
const lngType = rangeType(-180, 180);

const geoType = T.object({
  lat: latType,
  lng: lngType,
});

const latLngType = T.tuple(T.asNumber, T.asNumber);

// It will take a string as an input and it will return `{ lat: number, lng: number }` as an output.
const geoStrType = T.map(T.string, (value) => {
  const result = latLngType(value.split(","));
  return result.success
    ? geoType({ lat: result.value[0], lng: result.value[1] })
    : result;
});

const result = geoStrType("-39.031153, -67.576394"); // => { lat: -39.031153, lng: -67.576394 }

There is another utility function called fold which lets you run either a onLeft or onRight function depending on the result of the validation.

import * as T from "typed";

const userType = T.object({
  id: T.number,
  name: T.string,
});

type UserType = T.Infer<typeof userType>;

const fetcher = (path: string) =>
  fetch(path)
    .then((res) => res.json())
    .then(userType);

const renderErrors = (errors: T.Err[]) => (
  <ul>
    {errors.map((err, key) => (
      <li key={key}>{`${err.message} @ ${err.path.join(".")}`}</li>
    ))}
  </ul>
);

const renderProfile = (user: UserType) => (
  <div>
    <h1>{user.name}</h1>
    <p>{user.id}</p>
  </div>
);

const Profile: React.FC = () => {
  const { data } = useSWR("/api/users", fetcher);

  if (!data) {
    return <div>Loading...</div>;
  }

  return T.fold(data, renderErrors, renderProfile);
};

Inference

Sometimes you may want to infer the type of a validator function. You can do so with the Infer type.

import * as T from "typed";

const postType = T.object({
  id: T.number,
  title: T.string,
  tags: T.array(T.string),
});

type Post = T.Infer<typeof postType>; // => Post { id: number, title: string, tags: string[] }

Benchmark

typed is around 63% faster than superstruct and around 79% faster than zod when data is valid. Benchmarks were done on a Mac Mini (6-core, 3.0 GHz, 8 GB RAM) with Node.js 14.4.0 and TypeScript 4.4.4 using benny.

  superstruct (valid):
    3 486 ops/s, ±0.60%   | 63.32% slower

  zod (valid):
    1 961 ops/s, ±0.31%   | 79.37% slower

  typed (valid):
    9 505 ops/s, ±0.31%   | fastest

  superstruct (invalid):
    2 144 ops/s, ±2.68%   | 77.44% slower

  zod (invalid):
    1 250 ops/s, ±1.43%   | slowest, 86.85% slower

  typed (invalid):
    8 451 ops/s, ±0.23%   | 11.09% slower

Finished 6 cases!
  Fastest: typed (valid)
  Slowest: zod (invalid)

Demo

GitHub

View Github