@nanots/guard

Smallest validation library ever made

Usage no npm install needed!

<script type="module">
  import nanotsGuard from 'https://cdn.skypack.dev/@nanots/guard';
</script>

README

@nanots/guard

guard does only one thing: It checks data structures at runtime, nothing else. It doesn't do type-casting nor throws any errors, just a true or false scenario. It is the smallest, lightest and fastest piece of code I could write compiling down to 21 LOC, the source code being around 113 LOC.

Note: guard is ment to be used on typescript projects (you can use it with vanilla javascript too) with the strictNullChecks flag set to true.

Installation

$ npm install @nanots/guard

Usage

// It's so small you can import all methods without worring about tree-shaking
import * as G from "@nanots/guard";

const isUser = G.rec({
  name: G.str,
  age: G.int,
  tags: G.arr(G.str),
});

// Some data returned by a JSON API
const data = JSON.parse(response);

if (isUser(data)) {
  // `isUser` is now a typescript type-guard so if the `if` statement is true, `data` shape will be inferred
  // by typescript and you'll have code auto-comletion

  data.name; // => string
  data.age; // => number
  data.tags; // => string[]
}

Adding functionallity

A guard is just a function that accepts an unknown value and asserts that it is some specific type. Since guard only includes primitives, you can build your own guards on top of it.

// guards/string.ts
import { Guard, str } from "@nanots/guard";

export const regex =
  (pattern: RegExp): Guard<string> =>
  (x): x is string =>
    str(x) && pattern.test(x);

const pattern =
  /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;

export const email = regex(pattern);

export const upper: Guard<string> = (x): x is string =>
  str(x) && x.split("").every((c) => c.toUpperCase() === c);

export const lower: Guard<string> = (x): x is string =>
  str(x) && x.split("").every((c) => c.toLowerCase() === c);
// guards/number.ts
import { Guard, num } from "@nanots/guard";

export const positive: Guard<number> = (x): x is number => num(x) && x > 0;
export const negative: Guard<number> = (x): x is number => num(x) && x < 0;

export const int: Guard<number> = (x): x is number =>
  num(x) && Number.isInteger(x);

export const float: Guard<number> = (x): x is number =>
  num(x) && !Number.isInteger(x);

export const eq =
  (n: number): Guard<number> =>
  (x): x is number =>
    num(x) && x === n;

export const gt =
  (n: number): Guard<number> =>
  (x): x is number =>
    num(x) && x > n;

export const gte =
  (n: number): Guard<number> =>
  (x): x is number =>
    num(x) && x >= n;

export const lt =
  (n: number): Guard<number> =>
  (x): x is number =>
    num(x) && x < n;

export const lte =
  (n: number): Guard<number> =>
  (x): x is number =>
    num(x) && x <= n;

export const between =
  (min: number, max: number): Guard<number> =>
  (x): x is number =>
    gte(min) && lte(max);
// guards/geo.ts
import { rec } from "@nanots/guard";
import { between } from "./number";

export const point = rec({
  lat: between(-90, 90),
  lng: between(-180, 180),
});

Benchmarks

I ran a simple benchmark against superstruct (which is already very lightweight and very fast) and here are the results:

@nanots/guard x 0.12 ops/sec ±2.68% (1393 runs sampled)
superstruct x 0.04 ops/sec ±0.97% (1319 runs sampled)

Alternatives

  • Superstruct
  • Zod