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